scheduler.ts 7.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. import { ErrorCodes, callWithErrorHandling } from './errorHandling'
  2. import { isArray } from '@vue/shared'
  3. import { ComponentPublicInstance } from './componentPublicInstance'
  4. export interface SchedulerJob {
  5. (): void
  6. /**
  7. * unique job id, only present on raw effects, e.g. component render effect
  8. */
  9. id?: number
  10. /**
  11. * Indicates whether the job is allowed to recursively trigger itself.
  12. * By default, a job cannot trigger itself because some built-in method calls,
  13. * e.g. Array.prototype.push actually performs reads as well (#1740) which
  14. * can lead to confusing infinite loops.
  15. * The allowed cases are component update functions and watch callbacks.
  16. * Component update functions may update child component props, which in turn
  17. * trigger flush: "pre" watch callbacks that mutates state that the parent
  18. * relies on (#1801). Watch callbacks doesn't track its dependencies so if it
  19. * triggers itself again, it's likely intentional and it is the user's
  20. * responsibility to perform recursive state mutation that eventually
  21. * stabilizes (#1727).
  22. */
  23. allowRecurse?: boolean
  24. }
  25. export type SchedulerCb = Function & { id?: number }
  26. export type SchedulerCbs = SchedulerCb | SchedulerCb[]
  27. let isFlushing = false
  28. let isFlushPending = false
  29. const queue: SchedulerJob[] = []
  30. let flushIndex = 0
  31. const pendingPreFlushCbs: SchedulerCb[] = []
  32. let activePreFlushCbs: SchedulerCb[] | null = null
  33. let preFlushIndex = 0
  34. const pendingPostFlushCbs: SchedulerCb[] = []
  35. let activePostFlushCbs: SchedulerCb[] | null = null
  36. let postFlushIndex = 0
  37. const resolvedPromise: Promise<any> = Promise.resolve()
  38. let currentFlushPromise: Promise<void> | null = null
  39. let currentPreFlushParentJob: SchedulerJob | null = null
  40. const RECURSION_LIMIT = 100
  41. type CountMap = Map<SchedulerJob | SchedulerCb, number>
  42. export function nextTick(
  43. this: ComponentPublicInstance | void,
  44. fn?: () => void
  45. ): Promise<void> {
  46. const p = currentFlushPromise || resolvedPromise
  47. return fn ? p.then(this ? fn.bind(this) : fn) : p
  48. }
  49. // #2768
  50. // Use binary-search to find a suitable position in the queue,
  51. // so that the queue maintains the increasing order of job's id,
  52. // which can prevent the job from being skipped and also can avoid repeated patching.
  53. function findInsertionIndex(job: SchedulerJob) {
  54. // the start index should be `flushIndex + 1`
  55. let start = flushIndex + 1
  56. let end = queue.length
  57. const jobId = getId(job)
  58. while (start < end) {
  59. const middle = (start + end) >>> 1
  60. const middleJobId = getId(queue[middle])
  61. middleJobId < jobId ? (start = middle + 1) : (end = middle)
  62. }
  63. return start
  64. }
  65. export function queueJob(job: SchedulerJob) {
  66. // the dedupe search uses the startIndex argument of Array.includes()
  67. // by default the search index includes the current job that is being run
  68. // so it cannot recursively trigger itself again.
  69. // if the job is a watch() callback, the search will start with a +1 index to
  70. // allow it recursively trigger itself - it is the user's responsibility to
  71. // ensure it doesn't end up in an infinite loop.
  72. if (
  73. (!queue.length ||
  74. !queue.includes(
  75. job,
  76. isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
  77. )) &&
  78. job !== currentPreFlushParentJob
  79. ) {
  80. const pos = findInsertionIndex(job)
  81. if (pos > -1) {
  82. queue.splice(pos, 0, job)
  83. } else {
  84. queue.push(job)
  85. }
  86. queueFlush()
  87. }
  88. }
  89. function queueFlush() {
  90. if (!isFlushing && !isFlushPending) {
  91. isFlushPending = true
  92. currentFlushPromise = resolvedPromise.then(flushJobs)
  93. }
  94. }
  95. export function invalidateJob(job: SchedulerJob) {
  96. const i = queue.indexOf(job)
  97. if (i > flushIndex) {
  98. queue.splice(i, 1)
  99. }
  100. }
  101. function queueCb(
  102. cb: SchedulerCbs,
  103. activeQueue: SchedulerCb[] | null,
  104. pendingQueue: SchedulerCb[],
  105. index: number
  106. ) {
  107. if (!isArray(cb)) {
  108. if (
  109. !activeQueue ||
  110. !activeQueue.includes(
  111. cb,
  112. (cb as SchedulerJob).allowRecurse ? index + 1 : index
  113. )
  114. ) {
  115. pendingQueue.push(cb)
  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. pendingQueue.push(...cb)
  122. }
  123. queueFlush()
  124. }
  125. export function queuePreFlushCb(cb: SchedulerCb) {
  126. queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
  127. }
  128. export function queuePostFlushCb(cb: SchedulerCbs) {
  129. queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
  130. }
  131. export function flushPreFlushCbs(
  132. seen?: CountMap,
  133. parentJob: SchedulerJob | null = null
  134. ) {
  135. if (pendingPreFlushCbs.length) {
  136. currentPreFlushParentJob = parentJob
  137. activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
  138. pendingPreFlushCbs.length = 0
  139. if (__DEV__) {
  140. seen = seen || new Map()
  141. }
  142. for (
  143. preFlushIndex = 0;
  144. preFlushIndex < activePreFlushCbs.length;
  145. preFlushIndex++
  146. ) {
  147. if (__DEV__) {
  148. checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
  149. }
  150. activePreFlushCbs[preFlushIndex]()
  151. }
  152. activePreFlushCbs = null
  153. preFlushIndex = 0
  154. currentPreFlushParentJob = null
  155. // recursively flush until it drains
  156. flushPreFlushCbs(seen, parentJob)
  157. }
  158. }
  159. export function flushPostFlushCbs(seen?: CountMap) {
  160. if (pendingPostFlushCbs.length) {
  161. const deduped = [...new Set(pendingPostFlushCbs)]
  162. pendingPostFlushCbs.length = 0
  163. // #1947 already has active queue, nested flushPostFlushCbs call
  164. if (activePostFlushCbs) {
  165. activePostFlushCbs.push(...deduped)
  166. return
  167. }
  168. activePostFlushCbs = deduped
  169. if (__DEV__) {
  170. seen = seen || new Map()
  171. }
  172. activePostFlushCbs.sort((a, b) => getId(a) - getId(b))
  173. for (
  174. postFlushIndex = 0;
  175. postFlushIndex < activePostFlushCbs.length;
  176. postFlushIndex++
  177. ) {
  178. if (__DEV__) {
  179. checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
  180. }
  181. activePostFlushCbs[postFlushIndex]()
  182. }
  183. activePostFlushCbs = null
  184. postFlushIndex = 0
  185. }
  186. }
  187. const getId = (job: SchedulerJob | SchedulerCb) =>
  188. job.id == null ? Infinity : job.id
  189. function flushJobs(seen?: CountMap) {
  190. isFlushPending = false
  191. isFlushing = true
  192. if (__DEV__) {
  193. seen = seen || new Map()
  194. }
  195. flushPreFlushCbs(seen)
  196. // Sort queue before flush.
  197. // This ensures that:
  198. // 1. Components are updated from parent to child. (because parent is always
  199. // created before the child so its render effect will have smaller
  200. // priority number)
  201. // 2. If a component is unmounted during a parent component's update,
  202. // its update can be skipped.
  203. queue.sort((a, b) => getId(a) - getId(b))
  204. try {
  205. for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
  206. const job = queue[flushIndex]
  207. if (job) {
  208. if (__DEV__) {
  209. checkRecursiveUpdates(seen!, job)
  210. }
  211. callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
  212. }
  213. }
  214. } finally {
  215. flushIndex = 0
  216. queue.length = 0
  217. flushPostFlushCbs(seen)
  218. isFlushing = false
  219. currentFlushPromise = null
  220. // some postFlushCb queued jobs!
  221. // keep flushing until it drains.
  222. if (queue.length || pendingPostFlushCbs.length) {
  223. flushJobs(seen)
  224. }
  225. }
  226. }
  227. function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob | SchedulerCb) {
  228. if (!seen.has(fn)) {
  229. seen.set(fn, 1)
  230. } else {
  231. const count = seen.get(fn)!
  232. if (count > RECURSION_LIMIT) {
  233. throw new Error(
  234. `Maximum recursive updates exceeded. ` +
  235. `This means you have a reactive effect that is mutating its own ` +
  236. `dependencies and thus recursively triggering itself. Possible sources ` +
  237. `include component template, render function, updated hook or ` +
  238. `watcher source function.`
  239. )
  240. } else {
  241. seen.set(fn, count + 1)
  242. }
  243. }
  244. }