scheduler.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299
  1. import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
  2. import { NOOP, isArray } from '@vue/shared'
  3. import { type GenericComponentInstance, 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. /**
  26. * @internal
  27. */
  28. export interface SchedulerJob extends Function {
  29. id?: number
  30. /**
  31. * flags can technically be undefined, but it can still be used in bitwise
  32. * operations just like 0.
  33. */
  34. flags?: SchedulerJobFlags
  35. /**
  36. * Attached by renderer.ts when setting up a component's render effect
  37. * Used to obtain component information when reporting max recursive updates.
  38. */
  39. i?: GenericComponentInstance
  40. }
  41. export type SchedulerJobs = SchedulerJob | SchedulerJob[]
  42. const queue: SchedulerJob[] = []
  43. let flushIndex = -1
  44. const pendingPostFlushCbs: SchedulerJob[] = []
  45. let activePostFlushCbs: SchedulerJob[] | null = null
  46. let postFlushIndex = 0
  47. const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
  48. let currentFlushPromise: Promise<void> | null = null
  49. const RECURSION_LIMIT = 100
  50. type CountMap = Map<SchedulerJob, number>
  51. export function nextTick<T = void, R = void>(
  52. this: T,
  53. fn?: (this: T) => R,
  54. ): Promise<Awaited<R>> {
  55. const p = currentFlushPromise || resolvedPromise
  56. return fn ? p.then(this ? fn.bind(this) : fn) : p
  57. }
  58. // Use binary-search to find a suitable position in the queue. The queue needs
  59. // to be sorted in increasing order of the job ids. This ensures that:
  60. // 1. Components are updated from parent to child. As the parent is always
  61. // created before the child it will always have a smaller id.
  62. // 2. If a component is unmounted during a parent component's update, its update
  63. // can be skipped.
  64. // A pre watcher will have the same id as its component's update job. The
  65. // watcher should be inserted immediately before the update job. This allows
  66. // watchers to be skipped if the component is unmounted by the parent update.
  67. function findInsertionIndex(id: number) {
  68. let start = flushIndex + 1
  69. let end = queue.length
  70. while (start < end) {
  71. const middle = (start + end) >>> 1
  72. const middleJob = queue[middle]
  73. const middleJobId = getId(middleJob)
  74. if (
  75. middleJobId < id ||
  76. (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
  77. ) {
  78. start = middle + 1
  79. } else {
  80. end = middle
  81. }
  82. }
  83. return start
  84. }
  85. /**
  86. * @internal for runtime-vapor only
  87. */
  88. export function queueJob(job: SchedulerJob): void {
  89. if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
  90. const jobId = getId(job)
  91. const lastJob = queue[queue.length - 1]
  92. if (
  93. !lastJob ||
  94. // fast path when the job id is larger than the tail
  95. (!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
  96. ) {
  97. queue.push(job)
  98. } else {
  99. queue.splice(findInsertionIndex(jobId), 0, job)
  100. }
  101. job.flags! |= SchedulerJobFlags.QUEUED
  102. queueFlush()
  103. }
  104. }
  105. function queueFlush() {
  106. if (!currentFlushPromise) {
  107. currentFlushPromise = resolvedPromise.then(flushJobs).catch(e => {
  108. currentFlushPromise = null
  109. throw e
  110. })
  111. }
  112. }
  113. export function queuePostFlushCb(cb: SchedulerJobs): void {
  114. if (!isArray(cb)) {
  115. if (activePostFlushCbs && cb.id === -1) {
  116. activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
  117. } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
  118. pendingPostFlushCbs.push(cb)
  119. cb.flags! |= SchedulerJobFlags.QUEUED
  120. }
  121. } else {
  122. // if cb is an array, it is a component lifecycle hook which can only be
  123. // triggered by a job, which is already deduped in the main queue, so
  124. // we can skip duplicate check here to improve perf
  125. pendingPostFlushCbs.push(...cb)
  126. }
  127. queueFlush()
  128. }
  129. export function flushPreFlushCbs(
  130. instance?: GenericComponentInstance,
  131. seen?: CountMap,
  132. // skip the current job
  133. i: number = flushIndex + 1,
  134. ): void {
  135. if (__DEV__) {
  136. seen = seen || new Map()
  137. }
  138. for (; i < queue.length; i++) {
  139. const cb = queue[i]
  140. if (cb && cb.flags! & SchedulerJobFlags.PRE) {
  141. if (instance && cb.id !== instance.uid) {
  142. continue
  143. }
  144. if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
  145. continue
  146. }
  147. queue.splice(i, 1)
  148. i--
  149. if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
  150. cb.flags! &= ~SchedulerJobFlags.QUEUED
  151. }
  152. cb()
  153. if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
  154. cb.flags! &= ~SchedulerJobFlags.QUEUED
  155. }
  156. }
  157. }
  158. }
  159. export function flushPostFlushCbs(seen?: CountMap): void {
  160. if (pendingPostFlushCbs.length) {
  161. const deduped = [...new Set(pendingPostFlushCbs)].sort(
  162. (a, b) => getId(a) - getId(b),
  163. )
  164. pendingPostFlushCbs.length = 0
  165. // #1947 already has active queue, nested flushPostFlushCbs call
  166. if (activePostFlushCbs) {
  167. activePostFlushCbs.push(...deduped)
  168. return
  169. }
  170. activePostFlushCbs = deduped
  171. if (__DEV__) {
  172. seen = seen || new Map()
  173. }
  174. for (
  175. postFlushIndex = 0;
  176. postFlushIndex < activePostFlushCbs.length;
  177. postFlushIndex++
  178. ) {
  179. const cb = activePostFlushCbs[postFlushIndex]
  180. if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
  181. continue
  182. }
  183. if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
  184. cb.flags! &= ~SchedulerJobFlags.QUEUED
  185. }
  186. if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) {
  187. try {
  188. cb()
  189. } finally {
  190. cb.flags! &= ~SchedulerJobFlags.QUEUED
  191. }
  192. }
  193. }
  194. activePostFlushCbs = null
  195. postFlushIndex = 0
  196. }
  197. }
  198. const getId = (job: SchedulerJob): number =>
  199. job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
  200. function flushJobs(seen?: CountMap) {
  201. if (__DEV__) {
  202. seen = seen || new Map()
  203. }
  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.flags! & SchedulerJobFlags.DISPOSED)) {
  216. if (__DEV__ && check(job)) {
  217. continue
  218. }
  219. if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
  220. job.flags! &= ~SchedulerJobFlags.QUEUED
  221. }
  222. callWithErrorHandling(
  223. job,
  224. job.i,
  225. job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
  226. )
  227. if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
  228. job.flags! &= ~SchedulerJobFlags.QUEUED
  229. }
  230. }
  231. }
  232. } finally {
  233. // If there was an error we still need to clear the QUEUED flags
  234. for (; flushIndex < queue.length; flushIndex++) {
  235. const job = queue[flushIndex]
  236. if (job) {
  237. job.flags! &= ~SchedulerJobFlags.QUEUED
  238. }
  239. }
  240. flushIndex = -1
  241. queue.length = 0
  242. flushPostFlushCbs(seen)
  243. currentFlushPromise = null
  244. // If new jobs have been added to either queue, keep flushing
  245. if (queue.length || pendingPostFlushCbs.length) {
  246. flushJobs(seen)
  247. }
  248. }
  249. }
  250. function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
  251. const count = seen.get(fn) || 0
  252. if (count > RECURSION_LIMIT) {
  253. const instance = fn.i
  254. const componentName = instance && getComponentName(instance.type)
  255. handleError(
  256. `Maximum recursive updates exceeded${
  257. componentName ? ` in component <${componentName}>` : ``
  258. }. ` +
  259. `This means you have a reactive effect that is mutating its own ` +
  260. `dependencies and thus recursively triggering itself. Possible sources ` +
  261. `include component template, render function, updated hook or ` +
  262. `watcher source function.`,
  263. null,
  264. ErrorCodes.APP_ERROR_HANDLER,
  265. )
  266. return true
  267. }
  268. seen.set(fn, count + 1)
  269. return false
  270. }