2
0

scheduler.ts 8.3 KB

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