scheduler.ts 9.0 KB

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