scheduler.ts 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251
  1. import { ErrorCodes, callWithErrorHandling } from './errorHandling'
  2. import { isArray } from '@vue/shared'
  3. export interface SchedulerJob {
  4. (): void
  5. /**
  6. * unique job id, only present on raw effects, e.g. component render effect
  7. */
  8. id?: number
  9. /**
  10. * Indicates whether the job is allowed to recursively trigger itself.
  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. allowRecurse?: boolean
  23. }
  24. export type SchedulerCb = Function & { id?: number }
  25. export type SchedulerCbs = SchedulerCb | SchedulerCb[]
  26. let isFlushing = false
  27. let isFlushPending = false
  28. const queue: (SchedulerJob | null)[] = []
  29. let flushIndex = 0
  30. const pendingPreFlushCbs: SchedulerCb[] = []
  31. let activePreFlushCbs: SchedulerCb[] | null = null
  32. let preFlushIndex = 0
  33. const pendingPostFlushCbs: SchedulerCb[] = []
  34. let activePostFlushCbs: SchedulerCb[] | null = null
  35. let postFlushIndex = 0
  36. const resolvedPromise: Promise<any> = Promise.resolve()
  37. let currentFlushPromise: Promise<void> | null = null
  38. let currentPreFlushParentJob: SchedulerJob | null = null
  39. const RECURSION_LIMIT = 100
  40. type CountMap = Map<SchedulerJob | SchedulerCb, number>
  41. export function nextTick(fn?: () => void): Promise<void> {
  42. const p = currentFlushPromise || resolvedPromise
  43. return fn ? p.then(fn) : p
  44. }
  45. export function queueJob(job: SchedulerJob) {
  46. // the dedupe search uses the startIndex argument of Array.includes()
  47. // by default the search index includes the current job that is being run
  48. // so it cannot recursively trigger itself again.
  49. // if the job is a watch() callback, the search will start with a +1 index to
  50. // allow it recursively trigger itself - it is the user's responsibility to
  51. // ensure it doesn't end up in an infinite loop.
  52. if (
  53. (!queue.length ||
  54. !queue.includes(
  55. job,
  56. isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
  57. )) &&
  58. job !== currentPreFlushParentJob
  59. ) {
  60. queue.push(job)
  61. queueFlush()
  62. }
  63. }
  64. function queueFlush() {
  65. if (!isFlushing && !isFlushPending) {
  66. isFlushPending = true
  67. currentFlushPromise = resolvedPromise.then(flushJobs)
  68. }
  69. }
  70. export function invalidateJob(job: SchedulerJob) {
  71. const i = queue.indexOf(job)
  72. if (i > -1) {
  73. queue[i] = null
  74. }
  75. }
  76. function queueCb(
  77. cb: SchedulerCbs,
  78. activeQueue: SchedulerCb[] | null,
  79. pendingQueue: SchedulerCb[],
  80. index: number
  81. ) {
  82. if (!isArray(cb)) {
  83. if (
  84. !activeQueue ||
  85. !activeQueue.includes(
  86. cb,
  87. (cb as SchedulerJob).allowRecurse ? index + 1 : index
  88. )
  89. ) {
  90. pendingQueue.push(cb)
  91. }
  92. } else {
  93. // if cb is an array, it is a component lifecycle hook which can only be
  94. // triggered by a job, which is already deduped in the main queue, so
  95. // we can skip duplicate check here to improve perf
  96. pendingQueue.push(...cb)
  97. }
  98. queueFlush()
  99. }
  100. export function queuePreFlushCb(cb: SchedulerCb) {
  101. queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
  102. }
  103. export function queuePostFlushCb(cb: SchedulerCbs) {
  104. queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
  105. }
  106. export function flushPreFlushCbs(
  107. seen?: CountMap,
  108. parentJob: SchedulerJob | null = null
  109. ) {
  110. if (pendingPreFlushCbs.length) {
  111. currentPreFlushParentJob = parentJob
  112. activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
  113. pendingPreFlushCbs.length = 0
  114. if (__DEV__) {
  115. seen = seen || new Map()
  116. }
  117. for (
  118. preFlushIndex = 0;
  119. preFlushIndex < activePreFlushCbs.length;
  120. preFlushIndex++
  121. ) {
  122. if (__DEV__) {
  123. checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
  124. }
  125. activePreFlushCbs[preFlushIndex]()
  126. }
  127. activePreFlushCbs = null
  128. preFlushIndex = 0
  129. currentPreFlushParentJob = null
  130. // recursively flush until it drains
  131. flushPreFlushCbs(seen, parentJob)
  132. }
  133. }
  134. export function flushPostFlushCbs(seen?: CountMap) {
  135. if (pendingPostFlushCbs.length) {
  136. const deduped = [...new Set(pendingPostFlushCbs)]
  137. pendingPostFlushCbs.length = 0
  138. // #1947 already has active queue, nested flushPostFlushCbs call
  139. if (activePostFlushCbs) {
  140. activePostFlushCbs.push(...deduped)
  141. return
  142. }
  143. activePostFlushCbs = deduped
  144. if (__DEV__) {
  145. seen = seen || new Map()
  146. }
  147. activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
  148. for (
  149. postFlushIndex = 0;
  150. postFlushIndex < activePostFlushCbs.length;
  151. postFlushIndex++
  152. ) {
  153. if (__DEV__) {
  154. checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
  155. }
  156. activePostFlushCbs[postFlushIndex]()
  157. }
  158. activePostFlushCbs = null
  159. postFlushIndex = 0
  160. }
  161. }
  162. const getId = (job: SchedulerJob | SchedulerCb) =>
  163. job.id == null ? Infinity : job.id
  164. function flushJobs(seen?: CountMap) {
  165. isFlushPending = false
  166. isFlushing = true
  167. if (__DEV__) {
  168. seen = seen || new Map()
  169. }
  170. flushPreFlushCbs(seen)
  171. // Sort queue before flush.
  172. // This ensures that:
  173. // 1. Components are updated from parent to child. (because parent is always
  174. // created before the child so its render effect will have smaller
  175. // priority number)
  176. // 2. If a component is unmounted during a parent component's update,
  177. // its update can be skipped.
  178. // Jobs can never be null before flush starts, since they are only invalidated
  179. // during execution of another flushed job.
  180. queue.sort((a, b) => getId(a!) - getId(b!))
  181. try {
  182. for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
  183. const job = queue[flushIndex]
  184. if (job) {
  185. if (__DEV__) {
  186. checkRecursiveUpdates(seen!, job)
  187. }
  188. callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  189. }
  190. }
  191. } finally {
  192. flushIndex = 0
  193. queue.length = 0
  194. flushPostFlushCbs(seen)
  195. isFlushing = false
  196. currentFlushPromise = null
  197. // some postFlushCb queued jobs!
  198. // keep flushing until it drains.
  199. if (queue.length || pendingPostFlushCbs.length) {
  200. flushJobs(seen)
  201. }
  202. }
  203. }
  204. function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob | SchedulerCb) {
  205. if (!seen.has(fn)) {
  206. seen.set(fn, 1)
  207. } else {
  208. const count = seen.get(fn)!
  209. if (count > RECURSION_LIMIT) {
  210. throw new Error(
  211. `Maximum recursive updates exceeded. ` +
  212. `This means you have a reactive effect that is mutating its own ` +
  213. `dependencies and thus recursively triggering itself. Possible sources ` +
  214. `include component template, render function, updated hook or ` +
  215. `watcher source function.`
  216. )
  217. } else {
  218. seen.set(fn, count + 1)
  219. }
  220. }
  221. }