scheduler.ts 8.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288
  1. import { ErrorCodes, callWithErrorHandling } from './errorHandling'
  2. import { Awaited, isArray, NOOP } from '@vue/shared'
  3. import { ComponentInternalInstance, getComponentName } from './component'
  4. import { warn } from './warning'
  5. export interface SchedulerJob extends Function {
  6. id?: number
  7. pre?: boolean
  8. active?: boolean
  9. computed?: boolean
  10. /**
  11. * Indicates whether the effect is allowed to recursively trigger itself
  12. * when managed by the scheduler.
  13. *
  14. * By default, a job cannot trigger itself because some built-in method calls,
  15. * e.g. Array.prototype.push actually performs reads as well (#1740) which
  16. * can lead to confusing infinite loops.
  17. * The allowed cases are component update functions and watch callbacks.
  18. * Component update functions may update child component props, which in turn
  19. * trigger flush: "pre" watch callbacks that mutates state that the parent
  20. * relies on (#1801). Watch callbacks doesn't track its dependencies so if it
  21. * triggers itself again, it's likely intentional and it is the user's
  22. * responsibility to perform recursive state mutation that eventually
  23. * stabilizes (#1727).
  24. */
  25. allowRecurse?: boolean
  26. /**
  27. * Attached by renderer.ts when setting up a component's render effect
  28. * Used to obtain component information when reporting max recursive updates.
  29. * dev only.
  30. */
  31. ownerInstance?: ComponentInternalInstance
  32. }
  33. export type SchedulerJobs = SchedulerJob | SchedulerJob[]
  34. let isFlushing = false
  35. let isFlushPending = false
  36. const queue: SchedulerJob[] = []
  37. let flushIndex = 0
  38. const pendingPostFlushCbs: SchedulerJob[] = []
  39. let activePostFlushCbs: SchedulerJob[] | null = null
  40. let postFlushIndex = 0
  41. const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
  42. let currentFlushPromise: Promise<void> | null = null
  43. const RECURSION_LIMIT = 100
  44. type CountMap = Map<SchedulerJob, number>
  45. export function nextTick<T = void, R = void>(
  46. this: T,
  47. fn?: (this: T) => R
  48. ): Promise<Awaited<R>> {
  49. const p = currentFlushPromise || resolvedPromise
  50. return fn ? p.then(this ? fn.bind(this) : fn) : p
  51. }
  52. // #2768
  53. // Use binary-search to find a suitable position in the queue,
  54. // so that the queue maintains the increasing order of job's id,
  55. // which can prevent the job from being skipped and also can avoid repeated patching.
  56. function findInsertionIndex(id: number) {
  57. // the start index should be `flushIndex + 1`
  58. let start = flushIndex + 1
  59. let end = queue.length
  60. while (start < end) {
  61. const middle = (start + end) >>> 1
  62. const middleJob = queue[middle]
  63. const middleJobId = getId(middleJob)
  64. if (middleJobId < id || (middleJobId === id && middleJob.pre)) {
  65. start = middle + 1
  66. } else {
  67. end = middle
  68. }
  69. }
  70. return start
  71. }
  72. export function queueJob(job: SchedulerJob) {
  73. // the dedupe search uses the startIndex argument of Array.includes()
  74. // by default the search index includes the current job that is being run
  75. // so it cannot recursively trigger itself again.
  76. // if the job is a watch() callback, the search will start with a +1 index to
  77. // allow it recursively trigger itself - it is the user's responsibility to
  78. // ensure it doesn't end up in an infinite loop.
  79. if (
  80. !queue.length ||
  81. !queue.includes(
  82. job,
  83. isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
  84. )
  85. ) {
  86. if (job.id == null) {
  87. queue.push(job)
  88. } else {
  89. queue.splice(findInsertionIndex(job.id), 0, job)
  90. }
  91. queueFlush()
  92. }
  93. }
  94. function queueFlush() {
  95. if (!isFlushing && !isFlushPending) {
  96. isFlushPending = true
  97. currentFlushPromise = resolvedPromise.then(flushJobs)
  98. }
  99. }
  100. export function invalidateJob(job: SchedulerJob) {
  101. const i = queue.indexOf(job)
  102. if (i > flushIndex) {
  103. queue.splice(i, 1)
  104. }
  105. }
  106. export function queuePostFlushCb(cb: SchedulerJobs) {
  107. if (!isArray(cb)) {
  108. if (
  109. !activePostFlushCbs ||
  110. !activePostFlushCbs.includes(
  111. cb,
  112. cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex
  113. )
  114. ) {
  115. pendingPostFlushCbs.push(cb)
  116. }
  117. } else {
  118. // if cb is an array, it is a component lifecycle hook which can only be
  119. // triggered by a job, which is already deduped in the main queue, so
  120. // we can skip duplicate check here to improve perf
  121. pendingPostFlushCbs.push(...cb)
  122. }
  123. queueFlush()
  124. }
  125. export function flushPreFlushCbs(
  126. instance?: ComponentInternalInstance,
  127. seen?: CountMap,
  128. // if currently flushing, skip the current job itself
  129. i = isFlushing ? flushIndex + 1 : 0
  130. ) {
  131. if (__DEV__) {
  132. seen = seen || new Map()
  133. }
  134. for (; i < queue.length; i++) {
  135. const cb = queue[i]
  136. if (cb && cb.pre) {
  137. if (instance && cb.id !== instance.uid) {
  138. continue
  139. }
  140. if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
  141. continue
  142. }
  143. queue.splice(i, 1)
  144. i--
  145. cb()
  146. }
  147. }
  148. }
  149. export function flushPostFlushCbs(seen?: CountMap) {
  150. if (pendingPostFlushCbs.length) {
  151. const deduped = [...new Set(pendingPostFlushCbs)]
  152. pendingPostFlushCbs.length = 0
  153. // #1947 already has active queue, nested flushPostFlushCbs call
  154. if (activePostFlushCbs) {
  155. activePostFlushCbs.push(...deduped)
  156. return
  157. }
  158. activePostFlushCbs = deduped
  159. if (__DEV__) {
  160. seen = seen || new Map()
  161. }
  162. activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
  163. for (
  164. postFlushIndex = 0;
  165. postFlushIndex < activePostFlushCbs.length;
  166. postFlushIndex++
  167. ) {
  168. if (
  169. __DEV__ &&
  170. checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
  171. ) {
  172. continue
  173. }
  174. activePostFlushCbs[postFlushIndex]()
  175. }
  176. activePostFlushCbs = null
  177. postFlushIndex = 0
  178. }
  179. }
  180. const getId = (job: SchedulerJob): number =>
  181. job.id == null ? Infinity : job.id
  182. const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
  183. const diff = getId(a) - getId(b)
  184. if (diff === 0) {
  185. if (a.pre && !b.pre) return -1
  186. if (b.pre && !a.pre) return 1
  187. }
  188. return diff
  189. }
  190. function flushJobs(seen?: CountMap) {
  191. isFlushPending = false
  192. isFlushing = true
  193. if (__DEV__) {
  194. seen = seen || new Map()
  195. }
  196. // Sort queue before flush.
  197. // This ensures that:
  198. // 1. Components are updated from parent to child. (because parent is always
  199. // created before the child so its render effect will have smaller
  200. // priority number)
  201. // 2. If a component is unmounted during a parent component's update,
  202. // its update can be skipped.
  203. queue.sort(comparator)
  204. // conditional usage of checkRecursiveUpdate must be determined out of
  205. // try ... catch block since Rollup by default de-optimizes treeshaking
  206. // inside try-catch. This can leave all warning code unshaked. Although
  207. // they would get eventually shaken by a minifier like terser, some minifiers
  208. // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
  209. const check = __DEV__
  210. ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
  211. : NOOP
  212. try {
  213. for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
  214. const job = queue[flushIndex]
  215. if (job && job.active !== false) {
  216. if (__DEV__ && check(job)) {
  217. continue
  218. }
  219. // console.log(`running:`, job.id)
  220. callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  221. }
  222. }
  223. } finally {
  224. flushIndex = 0
  225. queue.length = 0
  226. flushPostFlushCbs(seen)
  227. isFlushing = false
  228. currentFlushPromise = null
  229. // some postFlushCb queued jobs!
  230. // keep flushing until it drains.
  231. if (queue.length || pendingPostFlushCbs.length) {
  232. flushJobs(seen)
  233. }
  234. }
  235. }
  236. function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
  237. if (!seen.has(fn)) {
  238. seen.set(fn, 1)
  239. } else {
  240. const count = seen.get(fn)!
  241. if (count > RECURSION_LIMIT) {
  242. const instance = fn.ownerInstance
  243. const componentName = instance && getComponentName(instance.type)
  244. warn(
  245. `Maximum recursive updates exceeded${
  246. componentName ? ` in component <${componentName}>` : ``
  247. }. ` +
  248. `This means you have a reactive effect that is mutating its own ` +
  249. `dependencies and thus recursively triggering itself. Possible sources ` +
  250. `include component template, render function, updated hook or ` +
  251. `watcher source function.`
  252. )
  253. return true
  254. } else {
  255. seen.set(fn, count + 1)
  256. }
  257. }
  258. }