scheduler.ts 8.8 KB

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