diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index e6172e7f..97dd131e 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -436,6 +436,7 @@ describe('api: watch', () => { it('flush: pre watcher watching props should fire before child update', async () => { const a = ref(0) const b = ref(0) + const c = ref(0) const calls: string[] = [] const Comp = { @@ -444,11 +445,22 @@ describe('api: watch', () => { watch( () => props.a + props.b, () => { - calls.push('watcher') + calls.push('watcher 1') + c.value++ + }, + { flush: 'pre' } + ) + + // #1777 chained pre-watcher + watch( + c, + () => { + calls.push('watcher 2') }, { flush: 'pre' } ) return () => { + c.value calls.push('render') } } @@ -469,7 +481,7 @@ describe('api: watch', () => { a.value++ b.value++ await nextTick() - expect(calls).toEqual(['render', 'watcher', 'render']) + expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render']) }) it('deep', async () => { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 5e7849fe..edec7a74 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -1430,7 +1430,7 @@ function baseCreateRenderer( instance.next = null updateProps(instance, nextVNode.props, prevProps, optimized) updateSlots(instance, nextVNode.children) - runPreflushJobs() + runPreflushJobs(instance.update) } const patchChildren: PatchChildrenFn = ( diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index c45113d8..01afd808 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -27,6 +27,7 @@ let flushIndex = 0 let pendingPostFlushCbs: Function[] | null = null let pendingPostFlushIndex = 0 let hasPendingPreFlushJobs = false +let currentPreFlushParentJob: SchedulerJob | null = null const RECURSION_LIMIT = 100 type CountMap = Map @@ -44,9 +45,16 @@ export function queueJob(job: SchedulerJob) { // 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.cb ? flushIndex + 1 : flushIndex) + (!queue.length || + !queue.includes( + job, + isFlushing && job.cb ? flushIndex + 1 : flushIndex + )) && + job !== currentPreFlushParentJob ) { + if (job.id && job.id > 0) { + debugger + } queue.push(job) if ((job.id as number) < 0) hasPendingPreFlushJobs = true queueFlush() @@ -60,16 +68,27 @@ export function invalidateJob(job: SchedulerJob) { } } -export function runPreflushJobs() { +/** + * Run flush: 'pre' watcher callbacks. This is only called in + * `updateComponentPreRender` to cover the case where pre-flush watchers are + * triggered by the change of a component's props. This means the scheduler is + * already flushing and we are already inside the component's update effect, + * right when the render function is about to be called. So if the watcher + * triggers the same component to update, we don't want it to be queued (this + * is checked via `currentPreFlushParentJob`). + */ +export function runPreflushJobs(parentJob: SchedulerJob) { if (hasPendingPreFlushJobs) { + currentPreFlushParentJob = parentJob hasPendingPreFlushJobs = false - for (let job, i = queue.length - 1; i > flushIndex; i--) { + for (let job, i = flushIndex + 1; i < queue.length; i++) { job = queue[i] if (job && (job.id as number) < 0) { job() queue[i] = null } } + currentPreFlushParentJob = null } }