scheduler.ts 8.3 KB

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