mirror of https://github.com/vuejs/core.git
294 lines
8.8 KiB
TypeScript
294 lines
8.8 KiB
TypeScript
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
|
|
import { NOOP, isArray } from '@vue/shared'
|
|
import { type ComponentInternalInstance, getComponentName } from './component'
|
|
|
|
export enum SchedulerJobFlags {
|
|
QUEUED = 1 << 0,
|
|
PRE = 1 << 1,
|
|
/**
|
|
* 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).
|
|
*/
|
|
ALLOW_RECURSE = 1 << 2,
|
|
DISPOSED = 1 << 3,
|
|
}
|
|
|
|
export interface SchedulerJob extends Function {
|
|
id?: number
|
|
/**
|
|
* flags can technically be undefined, but it can still be used in bitwise
|
|
* operations just like 0.
|
|
*/
|
|
flags?: SchedulerJobFlags
|
|
/**
|
|
* Attached by renderer.ts when setting up a component's render effect
|
|
* Used to obtain component information when reporting max recursive updates.
|
|
*/
|
|
i?: ComponentInternalInstance
|
|
}
|
|
|
|
export type SchedulerJobs = SchedulerJob | SchedulerJob[]
|
|
|
|
let isFlushing = false
|
|
let isFlushPending = false
|
|
|
|
const queue: SchedulerJob[] = []
|
|
let flushIndex = 0
|
|
|
|
const pendingPostFlushCbs: SchedulerJob[] = []
|
|
let activePostFlushCbs: SchedulerJob[] | null = null
|
|
let postFlushIndex = 0
|
|
|
|
const resolvedPromise = /*@__PURE__*/ Promise.resolve() as Promise<any>
|
|
let currentFlushPromise: Promise<void> | null = null
|
|
|
|
const RECURSION_LIMIT = 100
|
|
type CountMap = Map<SchedulerJob, number>
|
|
|
|
export function nextTick<T = void, R = void>(
|
|
this: T,
|
|
fn?: (this: T) => R,
|
|
): Promise<Awaited<R>> {
|
|
const p = currentFlushPromise || resolvedPromise
|
|
return fn ? p.then(this ? fn.bind(this) : fn) : p
|
|
}
|
|
|
|
// Use binary-search to find a suitable position in the queue. The queue needs
|
|
// to be sorted in increasing order of the job ids. This ensures that:
|
|
// 1. Components are updated from parent to child. As the parent is always
|
|
// created before the child it will always have a smaller id.
|
|
// 2. If a component is unmounted during a parent component's update, its update
|
|
// can be skipped.
|
|
// A pre watcher will have the same id as its component's update job. The
|
|
// watcher should be inserted immediately before the update job. This allows
|
|
// watchers to be skipped if the component is unmounted by the parent update.
|
|
function findInsertionIndex(id: number) {
|
|
let start = isFlushing ? flushIndex + 1 : 0
|
|
let end = queue.length
|
|
|
|
while (start < end) {
|
|
const middle = (start + end) >>> 1
|
|
const middleJob = queue[middle]
|
|
const middleJobId = getId(middleJob)
|
|
if (
|
|
middleJobId < id ||
|
|
(middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
|
|
) {
|
|
start = middle + 1
|
|
} else {
|
|
end = middle
|
|
}
|
|
}
|
|
|
|
return start
|
|
}
|
|
|
|
export function queueJob(job: SchedulerJob): void {
|
|
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
|
|
const jobId = getId(job)
|
|
const lastJob = queue[queue.length - 1]
|
|
if (
|
|
!lastJob ||
|
|
// fast path when the job id is larger than the tail
|
|
(!(job.flags! & SchedulerJobFlags.PRE) && jobId >= getId(lastJob))
|
|
) {
|
|
queue.push(job)
|
|
} else {
|
|
queue.splice(findInsertionIndex(jobId), 0, job)
|
|
}
|
|
|
|
job.flags! |= SchedulerJobFlags.QUEUED
|
|
|
|
queueFlush()
|
|
}
|
|
}
|
|
|
|
function queueFlush() {
|
|
if (!isFlushing && !isFlushPending) {
|
|
isFlushPending = true
|
|
currentFlushPromise = resolvedPromise.then(flushJobs)
|
|
}
|
|
}
|
|
|
|
export function queuePostFlushCb(cb: SchedulerJobs): void {
|
|
if (!isArray(cb)) {
|
|
if (activePostFlushCbs && cb.id === -1) {
|
|
activePostFlushCbs.splice(postFlushIndex + 1, 0, cb)
|
|
} else if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
|
|
pendingPostFlushCbs.push(cb)
|
|
cb.flags! |= SchedulerJobFlags.QUEUED
|
|
}
|
|
} 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
|
|
pendingPostFlushCbs.push(...cb)
|
|
}
|
|
queueFlush()
|
|
}
|
|
|
|
export function flushPreFlushCbs(
|
|
instance?: ComponentInternalInstance,
|
|
seen?: CountMap,
|
|
// if currently flushing, skip the current job itself
|
|
i: number = isFlushing ? flushIndex + 1 : 0,
|
|
): void {
|
|
if (__DEV__) {
|
|
seen = seen || new Map()
|
|
}
|
|
for (; i < queue.length; i++) {
|
|
const cb = queue[i]
|
|
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
|
|
if (instance && cb.id !== instance.uid) {
|
|
continue
|
|
}
|
|
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
|
|
continue
|
|
}
|
|
queue.splice(i, 1)
|
|
i--
|
|
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
|
|
cb.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
cb()
|
|
cb.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
}
|
|
}
|
|
|
|
export function flushPostFlushCbs(seen?: CountMap): void {
|
|
if (pendingPostFlushCbs.length) {
|
|
const deduped = [...new Set(pendingPostFlushCbs)].sort(
|
|
(a, b) => getId(a) - getId(b),
|
|
)
|
|
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()
|
|
}
|
|
|
|
for (
|
|
postFlushIndex = 0;
|
|
postFlushIndex < activePostFlushCbs.length;
|
|
postFlushIndex++
|
|
) {
|
|
const cb = activePostFlushCbs[postFlushIndex]
|
|
if (__DEV__ && checkRecursiveUpdates(seen!, cb)) {
|
|
continue
|
|
}
|
|
if (cb.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
|
|
cb.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
if (!(cb.flags! & SchedulerJobFlags.DISPOSED)) cb()
|
|
cb.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
activePostFlushCbs = null
|
|
postFlushIndex = 0
|
|
}
|
|
}
|
|
|
|
const getId = (job: SchedulerJob): number =>
|
|
job.id == null ? (job.flags! & SchedulerJobFlags.PRE ? -1 : Infinity) : job.id
|
|
|
|
function flushJobs(seen?: CountMap) {
|
|
isFlushPending = false
|
|
isFlushing = true
|
|
if (__DEV__) {
|
|
seen = seen || new Map()
|
|
}
|
|
|
|
// 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.flags! & SchedulerJobFlags.DISPOSED)) {
|
|
if (__DEV__ && check(job)) {
|
|
continue
|
|
}
|
|
if (job.flags! & SchedulerJobFlags.ALLOW_RECURSE) {
|
|
job.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
callWithErrorHandling(
|
|
job,
|
|
job.i,
|
|
job.i ? ErrorCodes.COMPONENT_UPDATE : ErrorCodes.SCHEDULER,
|
|
)
|
|
job.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
}
|
|
} finally {
|
|
// If there was an error we still need to clear the QUEUED flags
|
|
for (; flushIndex < queue.length; flushIndex++) {
|
|
const job = queue[flushIndex]
|
|
if (job) {
|
|
job.flags! &= ~SchedulerJobFlags.QUEUED
|
|
}
|
|
}
|
|
|
|
flushIndex = 0
|
|
queue.length = 0
|
|
|
|
flushPostFlushCbs(seen)
|
|
|
|
isFlushing = false
|
|
currentFlushPromise = null
|
|
// some postFlushCb queued jobs!
|
|
// keep flushing until it drains.
|
|
if (queue.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.i
|
|
const componentName = instance && getComponentName(instance.type)
|
|
handleError(
|
|
`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.`,
|
|
null,
|
|
ErrorCodes.APP_ERROR_HANDLER,
|
|
)
|
|
return true
|
|
} else {
|
|
seen.set(fn, count + 1)
|
|
}
|
|
}
|
|
}
|