diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index 9d8f4193..9e0e1043 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -278,4 +278,20 @@ describe('scheduler', () => { await nextTick() expect(calls).toEqual(['job3', 'job2', 'job1']) }) + + // #1595 + test('avoid duplicate postFlushCb invocation', async () => { + const calls: string[] = [] + const cb1 = () => { + calls.push('cb1') + queuePostFlushCb(cb2) + } + const cb2 = () => { + calls.push('cb2') + } + queuePostFlushCb(cb1) + queuePostFlushCb(cb2) + await nextTick() + expect(calls).toEqual(['cb1', 'cb2']) + }) }) diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index e518427d..8a775b39 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -12,6 +12,9 @@ const p = Promise.resolve() let isFlushing = false let isFlushPending = false +let flushIndex = 0 +let pendingPostFlushCbs: Function[] | null = null +let pendingPostFlushIndex = 0 const RECURSION_LIMIT = 100 type CountMap = Map @@ -21,7 +24,7 @@ export function nextTick(fn?: () => void): Promise { } export function queueJob(job: Job) { - if (!queue.includes(job)) { + if (!queue.includes(job, flushIndex)) { queue.push(job) queueFlush() } @@ -36,8 +39,16 @@ export function invalidateJob(job: Job) { export function queuePostFlushCb(cb: Function | Function[]) { if (!isArray(cb)) { - postFlushCbs.push(cb) + if ( + !pendingPostFlushCbs || + !pendingPostFlushCbs.includes(cb, 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() @@ -52,17 +63,23 @@ function queueFlush() { export function flushPostFlushCbs(seen?: CountMap) { if (postFlushCbs.length) { - const cbs = [...new Set(postFlushCbs)] + pendingPostFlushCbs = [...new Set(postFlushCbs)] postFlushCbs.length = 0 if (__DEV__) { seen = seen || new Map() } - for (let i = 0; i < cbs.length; i++) { + for ( + pendingPostFlushIndex = 0; + pendingPostFlushIndex < pendingPostFlushCbs.length; + pendingPostFlushIndex++ + ) { if (__DEV__) { - checkRecursiveUpdates(seen!, cbs[i]) + checkRecursiveUpdates(seen!, pendingPostFlushCbs[pendingPostFlushIndex]) } - cbs[i]() + pendingPostFlushCbs[pendingPostFlushIndex]() } + pendingPostFlushCbs = null + pendingPostFlushIndex = 0 } } @@ -71,7 +88,6 @@ const getId = (job: Job) => (job.id == null ? Infinity : job.id) function flushJobs(seen?: CountMap) { isFlushPending = false isFlushing = true - let job if (__DEV__) { seen = seen || new Map() } @@ -87,15 +103,18 @@ function flushJobs(seen?: CountMap) { // during execution of another flushed job. queue.sort((a, b) => getId(a!) - getId(b!)) - while ((job = queue.shift()) !== undefined) { - if (job === null) { - continue + for (flushIndex = 0; flushIndex < queue.length; flushIndex++) { + const job = queue[flushIndex] + if (job) { + if (__DEV__) { + checkRecursiveUpdates(seen!, job) + } + callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } - if (__DEV__) { - checkRecursiveUpdates(seen!, job) - } - callWithErrorHandling(job, null, ErrorCodes.SCHEDULER) } + flushIndex = 0 + queue.length = 0 + flushPostFlushCbs(seen) isFlushing = false // some postFlushCb queued jobs!