import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { isArray, NOOP } from '@vue/shared' import { ComponentInternalInstance, getComponentName } from './component' import { warn } from './warning' export interface SchedulerJob extends Function { id?: number active?: boolean computed?: boolean /** * Indicates whether the effect is allowed to recursively trigger itself * when managed by the scheduler. * * By default, a job cannot trigger itself because some built-in method calls, * e.g. Array.prototype.push actually performs reads as well (#1740) which * can lead to confusing infinite loops. * The allowed cases are component update functions and watch callbacks. * Component update functions may update child component props, which in turn * trigger flush: "pre" watch callbacks that mutates state that the parent * relies on (#1801). Watch callbacks doesn't track its dependencies so if it * triggers itself again, it's likely intentional and it is the user's * responsibility to perform recursive state mutation that eventually * stabilizes (#1727). */ allowRecurse?: boolean /** * Attached by renderer.ts when setting up a component's render effect * Used to obtain component information when reporting max recursive updates. * dev only. */ ownerInstance?: ComponentInternalInstance } export type SchedulerJobs = SchedulerJob | SchedulerJob[] let isFlushing = false let isFlushPending = false const queue: SchedulerJob[] = [] let flushIndex = 0 const pendingPreFlushCbs: SchedulerJob[] = [] let activePreFlushCbs: SchedulerJob[] | null = null let preFlushIndex = 0 const pendingPostFlushCbs: SchedulerJob[] = [] let activePostFlushCbs: SchedulerJob[] | null = null let postFlushIndex = 0 const resolvedPromise: Promise = Promise.resolve() let currentFlushPromise: Promise | null = null let currentPreFlushParentJob: SchedulerJob | null = null const RECURSION_LIMIT = 100 type CountMap = Map export function nextTick( this: T, fn?: (this: T) => void ): Promise { const p = currentFlushPromise || resolvedPromise return fn ? p.then(this ? fn.bind(this) : fn) : p } // #2768 // Use binary-search to find a suitable position in the queue, // so that the queue maintains the increasing order of job's id, // which can prevent the job from being skipped and also can avoid repeated patching. function findInsertionIndex(id: number) { // the start index should be `flushIndex + 1` let start = flushIndex + 1 let end = queue.length while (start < end) { const middle = (start + end) >>> 1 const middleJobId = getId(queue[middle]) middleJobId < id ? (start = middle + 1) : (end = middle) } return start } export function queueJob(job: SchedulerJob) { // the dedupe search uses the startIndex argument of Array.includes() // by default the search index includes the current job that is being run // so it cannot recursively trigger itself again. // if the job is a watch() callback, the search will start with a +1 index to // allow it recursively trigger itself - it is the user's responsibility to // ensure it doesn't end up in an infinite loop. if ( (!queue.length || !queue.includes( job, isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex )) && job !== currentPreFlushParentJob ) { if (job.id == null) { queue.push(job) } else { queue.splice(findInsertionIndex(job.id), 0, job) } queueFlush() } } function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } } export function invalidateJob(job: SchedulerJob) { const i = queue.indexOf(job) if (i > flushIndex) { queue.splice(i, 1) } } function queueCb( cb: SchedulerJobs, activeQueue: SchedulerJob[] | null, pendingQueue: SchedulerJob[], index: number ) { if (!isArray(cb)) { if ( !activeQueue || !activeQueue.includes(cb, cb.allowRecurse ? index + 1 : index) ) { pendingQueue.push(cb) } } else { // if cb is an array, it is a component lifecycle hook which can only be // triggered by a job, which is already deduped in the main queue, so // we can skip duplicate check here to improve perf pendingQueue.push(...cb) } queueFlush() } export function queuePreFlushCb(cb: SchedulerJob) { queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex) } export function queuePostFlushCb(cb: SchedulerJobs) { queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex) } export function flushPreFlushCbs( seen?: CountMap, parentJob: SchedulerJob | null = null ) { if (pendingPreFlushCbs.length) { currentPreFlushParentJob = parentJob activePreFlushCbs = [...new Set(pendingPreFlushCbs)] pendingPreFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() } for ( preFlushIndex = 0; preFlushIndex < activePreFlushCbs.length; preFlushIndex++ ) { if ( __DEV__ && checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex]) ) { continue } activePreFlushCbs[preFlushIndex]() } activePreFlushCbs = null preFlushIndex = 0 currentPreFlushParentJob = null // recursively flush until it drains flushPreFlushCbs(seen, parentJob) } } export function flushPostFlushCbs(seen?: CountMap) { if (pendingPostFlushCbs.length) { const deduped = [...new Set(pendingPostFlushCbs)] pendingPostFlushCbs.length = 0 // #1947 already has active queue, nested flushPostFlushCbs call if (activePostFlushCbs) { activePostFlushCbs.push(...deduped) return } activePostFlushCbs = deduped if (__DEV__) { seen = seen || new Map() } activePostFlushCbs.sort((a, b) => getId(a) - getId(b)) for ( postFlushIndex = 0; postFlushIndex < activePostFlushCbs.length; postFlushIndex++ ) { if ( __DEV__ && checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex]) ) { continue } activePostFlushCbs[postFlushIndex]() } activePostFlushCbs = null postFlushIndex = 0 } } const getId = (job: SchedulerJob): number => job.id == null ? Infinity : job.id function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || new Map() } flushPreFlushCbs(seen) // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child so its render effect will have smaller // priority number) // 2. If a component is unmounted during a parent component's update, // its update can be skipped. queue.sort((a, b) => getId(a) - getId(b)) // conditional usage of checkRecursiveUpdate must be determined out of // try ... catch block since Rollup by default de-optimizes treeshaking // inside try-catch. This can leave all warning code unshaked. Although // they would get eventually shaken by a minifier like terser, some minifiers // would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610) const check = __DEV__ ? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job) : NOOP try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job && job.active !== false) { if (__DEV__ && check(job)) { continue } // console.log(`running:`, job.id) callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } } } finally { flushIndex = 0 queue.length = 0 flushPostFlushCbs(seen) isFlushing = false currentFlushPromise = null // some postFlushCb queued jobs! // keep flushing until it drains. if ( queue.length || pendingPreFlushCbs.length || pendingPostFlushCbs.length ) { flushJobs(seen) } } } function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) { if (!seen.has(fn)) { seen.set(fn, 1) } else { const count = seen.get(fn)! if (count > RECURSION_LIMIT) { const instance = fn.ownerInstance const componentName = instance && getComponentName(instance.type) warn( `Maximum recursive updates exceeded${ componentName ? ` in component <${componentName}>` : `` }. ` + `This means you have a reactive effect that is mutating its own ` + `dependencies and thus recursively triggering itself. Possible sources ` + `include component template, render function, updated hook or ` + `watcher source function.` ) return true } else { seen.set(fn, count + 1) } } }