scheduler.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  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 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. let isFlushing = false
  40. let isFlushPending = false
  41. const queue: SchedulerJob[] = []
  42. let flushIndex = 0
  43. const pendingPostFlushCbs: SchedulerJob[] = []
  44. let activePostFlushCbs: SchedulerJob[] | null = null
  45. let postFlushIndex = 0
  46. const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
  47. let currentFlushPromise: Promise<void> | null = null
  48. const RECURSION_LIMIT = 100
  49. type CountMap = Map<SchedulerJob, number>
  50. export function nextTick<T = void, R = void>(
  51. this: T,
  52. fn?: (this: T) => R,
  53. ): Promise<Awaited<R>> {
  54. const p = currentFlushPromise || resolvedPromise
  55. return fn ? p.then(this ? fn.bind(this) : fn) : p
  56. }
  57. // #2768
  58. // Use binary-search to find a suitable position in the queue,
  59. // so that the queue maintains the increasing order of job's id,
  60. // which can prevent the job from being skipped and also can avoid repeated patching.
  61. function findInsertionIndex(id: number) {
  62. // the start index should be `flushIndex + 1`
  63. let start = flushIndex + 1
  64. let end = queue.length
  65. while (start < end) {
  66. const middle = (start + end) >>> 1
  67. const middleJob = queue[middle]
  68. const middleJobId = getId(middleJob)
  69. if (
  70. middleJobId < id ||
  71. (middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
  72. ) {
  73. start = middle + 1
  74. } else {
  75. end = middle
  76. }
  77. }
  78. return start
  79. }
  80. export function queueJob(job: SchedulerJob): void {
  81. if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
  82. const jobId = getId(job)
  83. const lastJob = queue[queue.length - 1]
  84. if (
  85. !lastJob ||
  86. // fast path when the job id is larger than the tail
  87. (!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
  88. ) {
  89. queue.push(job)
  90. } else {
  91. queue.splice(findInsertionIndex(jobId), 0, job)
  92. }
  93. if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
  94. job.flags! |= SchedulerJobFlags.QUEUED
  95. }
  96. queueFlush()
  97. }
  98. }
  99. function queueFlush() {
  100. if (!isFlushing && !isFlushPending) {
  101. isFlushPending = true
  102. currentFlushPromise = resolvedPromise.then(flushJobs)
  103. }
  104. }
  105. export function invalidateJob(job: SchedulerJob): void {
  106. const i = queue.indexOf(job)
  107. if (i > flushIndex) {
  108. queue.splice(i, 1)
  109. }
  110. }
  111. export function queuePostFlushCb(cb: SchedulerJobs): void {
  112. if (!isArray(cb)) {
  113. if (activePostFlushCbs && cb.id === -1) {
  114. activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
  115. } else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
  116. pendingPostFlushCbs.push(cb)
  117. if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
  118. cb.flags! |= SchedulerJobFlags.QUEUED
  119. }
  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?: ComponentInternalInstance,
  131. seen?: CountMap,
  132. // if currently flushing, skip the current job itself
  133. i: number = isFlushing ? flushIndex + 1 : 0,
  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. cb()
  150. cb.flags! &= ~SchedulerJobFlags.QUEUED
  151. }
  152. }
  153. }
  154. export function flushPostFlushCbs(seen?: CountMap): void {
  155. if (pendingPostFlushCbs.length) {
  156. const deduped = [...new Set(pendingPostFlushCbs)].sort(
  157. (a, b) => getId(a) - getId(b),
  158. )
  159. pendingPostFlushCbs.length = 0
  160. // #1947 already has active queue, nested flushPostFlushCbs call
  161. if (activePostFlushCbs) {
  162. activePostFlushCbs.push(...deduped)
  163. return
  164. }
  165. activePostFlushCbs = deduped
  166. if (__DEV__) {
  167. seen = seen || new Map()
  168. }
  169. for (
  170. postFlushIndex = 0;
  171. postFlushIndex < activePostFlushCbs.length;
  172. postFlushIndex++
  173. ) {
  174. const cb = activePostFlushCbs[postFlushIndex]
  175. if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
  176. continue
  177. }
  178. if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
  179. cb.flags! &= ~SchedulerJobFlags.QUEUED
  180. }
  181. activePostFlushCbs = null
  182. postFlushIndex = 0
  183. }
  184. }
  185. const getId = (job: SchedulerJob): number =>
  186. job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
  187. const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
  188. const diff = getId(a) - getId(b)
  189. if (diff === 0) {
  190. const isAPre = a.flags! & SchedulerJobFlags.PRE
  191. const isBPre = b.flags! & SchedulerJobFlags.PRE
  192. if (isAPre && !isBPre) return -1
  193. if (isBPre && !isAPre) return 1
  194. }
  195. return diff
  196. }
  197. function flushJobs(seen?: CountMap) {
  198. isFlushPending = false
  199. isFlushing = true
  200. if (__DEV__) {
  201. seen = seen || new Map()
  202. }
  203. // Sort queue before flush.
  204. // This ensures that:
  205. // 1. Components are updated from parent to child. (because parent is always
  206. // created before the child so its render effect will have smaller
  207. // priority number)
  208. // 2. If a component is unmounted during a parent component's update,
  209. // its update can be skipped.
  210. queue.sort(comparator)
  211. // conditional usage of checkRecursiveUpdate must be determined out of
  212. // try ... catch block since Rollup by default de-optimizes treeshaking
  213. // inside try-catch. This can leave all warning code unshaked. Although
  214. // they would get eventually shaken by a minifier like terser, some minifiers
  215. // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
  216. const check = __DEV__
  217. ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
  218. : NOOP
  219. try {
  220. for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
  221. const job = queue[flushIndex]
  222. if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
  223. if (__DEV__ && check(job)) {
  224. continue
  225. }
  226. callWithErrorHandling(
  227. job,
  228. job.i,
  229. job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
  230. )
  231. job.flags! &= ~SchedulerJobFlags.QUEUED
  232. }
  233. }
  234. } finally {
  235. flushIndex = 0
  236. queue.length = 0
  237. flushPostFlushCbs(seen)
  238. isFlushing = false
  239. currentFlushPromise = null
  240. // some postFlushCb queued jobs!
  241. // keep flushing until it drains.
  242. if (queue.length || pendingPostFlushCbs.length) {
  243. flushJobs(seen)
  244. }
  245. }
  246. }
  247. function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
  248. if (!seen.has(fn)) {
  249. seen.set(fn, 1)
  250. } else {
  251. const count = seen.get(fn)!
  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. } else {
  268. seen.set(fn, count + 1)
  269. }
  270. }
  271. }