From d4c17fb48b7880a4e3db6d48f8ab76540a3f59a2 Mon Sep 17 00:00:00 2001 From: Evan You Date: Mon, 3 Aug 2020 16:49:13 -0400 Subject: [PATCH] fix(watch): pre-flush watcher watching props should trigger before component update fix #1763 --- .../runtime-core/__tests__/apiWatch.spec.ts | 40 +++++++++++++++++++ packages/runtime-core/src/renderer.ts | 4 +- packages/runtime-core/src/scheduler.ts | 15 +++++++ 3 files changed, 58 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 97f686ad995..e6172e7f568 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -432,6 +432,46 @@ describe('api: watch', () => { expect(cb).toHaveBeenCalledTimes(1) }) + // #1763 + it('flush: pre watcher watching props should fire before child update', async () => { + const a = ref(0) + const b = ref(0) + const calls: string[] = [] + + const Comp = { + props: ['a', 'b'], + setup(props: any) { + watch( + () => props.a + props.b, + () => { + calls.push('watcher') + }, + { flush: 'pre' } + ) + return () => { + calls.push('render') + } + } + } + + const App = { + render() { + return h(Comp, { a: a.value, b: b.value }) + } + } + + render(h(App), nodeOps.createElement('div')) + expect(calls).toEqual(['render']) + + // both props are updated + // should trigger pre-flush watcher first and only once + // then trigger child render + a.value++ + b.value++ + await nextTick() + expect(calls).toEqual(['render', 'watcher', 'render']) + }) + it('deep', async () => { const state = reactive({ nested: { diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index ed832253182..5e7849feba5 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -40,7 +40,8 @@ import { queueJob, queuePostFlushCb, flushPostFlushCbs, - invalidateJob + invalidateJob, + runPreflushJobs } from './scheduler' import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity' import { updateProps } from './componentProps' @@ -1429,6 +1430,7 @@ function baseCreateRenderer( instance.next = null updateProps(instance, nextVNode.props, prevProps, optimized) updateSlots(instance, nextVNode.children) + runPreflushJobs() } const patchChildren: PatchChildrenFn = ( diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index a6933bcccd5..cbfedf0a07c 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -26,6 +26,7 @@ let isFlushPending = false let flushIndex = 0 let pendingPostFlushCbs: Function[] | null = null let pendingPostFlushIndex = 0 +let hasPendingPreFlushJobs = false const RECURSION_LIMIT = 100 type CountMap = Map @@ -47,6 +48,7 @@ export function queueJob(job: SchedulerJob) { !queue.includes(job, job.cb ? flushIndex + 1 : flushIndex) ) { queue.push(job) + if ((job.id as number) < 0) hasPendingPreFlushJobs = true queueFlush() } } @@ -58,6 +60,19 @@ export function invalidateJob(job: SchedulerJob) { } } +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 (