Skip to content

Commit a0e34ce

Browse files
committed
fix(watch): exhaust pre-flush watchers + avoid duplicate render by pre-flush watchers
close #1777
1 parent b5f91ff commit a0e34ce

File tree

3 files changed

+38
-7
lines changed

3 files changed

+38
-7
lines changed

packages/runtime-core/__tests__/apiWatch.spec.ts

+14-2
Original file line numberDiff line numberDiff line change
@@ -436,6 +436,7 @@ describe('api: watch', () => {
436436
it('flush: pre watcher watching props should fire before child update', async () => {
437437
const a = ref(0)
438438
const b = ref(0)
439+
const c = ref(0)
439440
const calls: string[] = []
440441

441442
const Comp = {
@@ -444,11 +445,22 @@ describe('api: watch', () => {
444445
watch(
445446
() => props.a + props.b,
446447
() => {
447-
calls.push('watcher')
448+
calls.push('watcher 1')
449+
c.value++
450+
},
451+
{ flush: 'pre' }
452+
)
453+
454+
// #1777 chained pre-watcher
455+
watch(
456+
c,
457+
() => {
458+
calls.push('watcher 2')
448459
},
449460
{ flush: 'pre' }
450461
)
451462
return () => {
463+
c.value
452464
calls.push('render')
453465
}
454466
}
@@ -469,7 +481,7 @@ describe('api: watch', () => {
469481
a.value++
470482
b.value++
471483
await nextTick()
472-
expect(calls).toEqual(['render', 'watcher', 'render'])
484+
expect(calls).toEqual(['render', 'watcher 1', 'watcher 2', 'render'])
473485
})
474486

475487
it('deep', async () => {

packages/runtime-core/src/renderer.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1430,7 +1430,7 @@ function baseCreateRenderer(
14301430
instance.next = null
14311431
updateProps(instance, nextVNode.props, prevProps, optimized)
14321432
updateSlots(instance, nextVNode.children)
1433-
runPreflushJobs()
1433+
runPreflushJobs(instance.update)
14341434
}
14351435

14361436
const patchChildren: PatchChildrenFn = (

packages/runtime-core/src/scheduler.ts

+23-4
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ let flushIndex = 0
2727
let pendingPostFlushCbs: Function[] | null = null
2828
let pendingPostFlushIndex = 0
2929
let hasPendingPreFlushJobs = false
30+
let currentPreFlushParentJob: SchedulerJob | null = null
3031

3132
const RECURSION_LIMIT = 100
3233
type CountMap = Map<SchedulerJob | Function, number>
@@ -44,9 +45,16 @@ export function queueJob(job: SchedulerJob) {
4445
// allow it recursively trigger itself - it is the user's responsibility to
4546
// ensure it doesn't end up in an infinite loop.
4647
if (
47-
!queue.length ||
48-
!queue.includes(job, isFlushing && job.cb ? flushIndex + 1 : flushIndex)
48+
(!queue.length ||
49+
!queue.includes(
50+
job,
51+
isFlushing && job.cb ? flushIndex + 1 : flushIndex
52+
)) &&
53+
job !== currentPreFlushParentJob
4954
) {
55+
if (job.id && job.id > 0) {
56+
debugger
57+
}
5058
queue.push(job)
5159
if ((job.id as number) < 0) hasPendingPreFlushJobs = true
5260
queueFlush()
@@ -60,16 +68,27 @@ export function invalidateJob(job: SchedulerJob) {
6068
}
6169
}
6270

63-
export function runPreflushJobs() {
71+
/**
72+
* Run flush: 'pre' watcher callbacks. This is only called in
73+
* `updateComponentPreRender` to cover the case where pre-flush watchers are
74+
* triggered by the change of a component's props. This means the scheduler is
75+
* already flushing and we are already inside the component's update effect,
76+
* right when the render function is about to be called. So if the watcher
77+
* triggers the same component to update, we don't want it to be queued (this
78+
* is checked via `currentPreFlushParentJob`).
79+
*/
80+
export function runPreflushJobs(parentJob: SchedulerJob) {
6481
if (hasPendingPreFlushJobs) {
82+
currentPreFlushParentJob = parentJob
6583
hasPendingPreFlushJobs = false
66-
for (let job, i = queue.length - 1; i > flushIndex; i--) {
84+
for (let job, i = flushIndex + 1; i < queue.length; i++) {
6785
job = queue[i]
6886
if (job && (job.id as number) < 0) {
6987
job()
7088
queue[i] = null
7189
}
7290
}
91+
currentPreFlushParentJob = null
7392
}
7493
}
7594

0 commit comments

Comments
 (0)