import { ErrorCodes, callWithErrorHandling } from './errorHandling' import { isArray } from '@vue/shared' export interface SchedulerJob { (): void /** * unique job id, only present on raw effects, e.g. component render effect */ id?: number /** * Indicates this is a watch() callback and is allowed to trigger itself. * A watch callback 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. */ cb?: boolean } const queue: (SchedulerJob | null)[] = [] const postFlushCbs: Function[] = [] const resolvedPromise: Promise = Promise.resolve() let currentFlushPromise: Promise | null = null let isFlushing = false let isFlushPending = false let flushIndex = 0 let pendingPostFlushCbs: Function[] | null = null let pendingPostFlushIndex = 0 let hasPendingPreFlushJobs = false const RECURSION_LIMIT = 100 type CountMap = Map export function nextTick(fn?: () => void): Promise { const p = currentFlushPromise || resolvedPromise return fn ? p.then(fn) : p } 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, job.cb ? flushIndex + 1 : flushIndex) ) { queue.push(job) if ((job.id as number) < 0) hasPendingPreFlushJobs = true queueFlush() } } export function invalidateJob(job: SchedulerJob) { const i = queue.indexOf(job) if (i > -1) { queue[i] = null } } export function runPreflushJobs() { if (hasPendingPreFlushJobs) { hasPendingPreFlushJobs = false for (let job, i = queue.length - 1; i > flushIndex; i--) { job = queue[i] if (job && (job.id as number) < 0) { job() queue[i] = null } } } } export function queuePostFlushCb(cb: Function | Function[]) { if (!isArray(cb)) { if ( !pendingPostFlushCbs || !pendingPostFlushCbs.includes( cb, (cb as SchedulerJob).cb ? pendingPostFlushIndex + 1 : pendingPostFlushIndex ) ) { postFlushCbs.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 dupicate check here to improve perf postFlushCbs.push(...cb) } queueFlush() } function queueFlush() { if (!isFlushing && !isFlushPending) { isFlushPending = true currentFlushPromise = resolvedPromise.then(flushJobs) } } export function flushPostFlushCbs(seen?: CountMap) { if (postFlushCbs.length) { pendingPostFlushCbs = [...new Set(postFlushCbs)] postFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() } for ( pendingPostFlushIndex = 0; pendingPostFlushIndex < pendingPostFlushCbs.length; pendingPostFlushIndex++ ) { if (__DEV__) { checkRecursiveUpdates(seen!, pendingPostFlushCbs[pendingPostFlushIndex]) } pendingPostFlushCbs[pendingPostFlushIndex]() } pendingPostFlushCbs = null pendingPostFlushIndex = 0 } } const getId = (job: SchedulerJob) => (job.id == null ? Infinity : job.id) function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true if (__DEV__) { seen = seen || new Map() } // 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. // Jobs can never be null before flush starts, since they are only invalidated // during execution of another flushed job. queue.sort((a, b) => getId(a!) - getId(b!)) try { for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { const job = queue[flushIndex] if (job) { if (__DEV__) { checkRecursiveUpdates(seen!, job) } 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 || postFlushCbs.length) { flushJobs(seen) } } } function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob | Function) { if (!seen.has(fn)) { seen.set(fn, 1) } else { const count = seen.get(fn)! if (count > RECURSION_LIMIT) { throw new Error( `Maximum recursive updates exceeded. ` + `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.` ) } else { seen.set(fn, count + 1) } } }