Skip to content

Commit 3692f27

Browse files
committed
refactor(runtime-core/scheduler): dedicated preFlush queue
properly fix #1763, #1777, #1781
1 parent 74a1265 commit 3692f27

File tree

4 files changed

+210
-62
lines changed

4 files changed

+210
-62
lines changed

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

+125-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import {
22
queueJob,
33
nextTick,
44
queuePostFlushCb,
5-
invalidateJob
5+
invalidateJob,
6+
queuePreFlushCb,
7+
flushPreFlushCbs
68
} from '../src/scheduler'
79

810
describe('scheduler', () => {
@@ -75,6 +77,128 @@ describe('scheduler', () => {
7577
})
7678
})
7779

80+
describe('queuePreFlushCb', () => {
81+
it('basic usage', async () => {
82+
const calls: string[] = []
83+
const cb1 = () => {
84+
calls.push('cb1')
85+
}
86+
const cb2 = () => {
87+
calls.push('cb2')
88+
}
89+
90+
queuePreFlushCb(cb1)
91+
queuePreFlushCb(cb2)
92+
93+
expect(calls).toEqual([])
94+
await nextTick()
95+
expect(calls).toEqual(['cb1', 'cb2'])
96+
})
97+
98+
it('should dedupe queued preFlushCb', async () => {
99+
const calls: string[] = []
100+
const cb1 = () => {
101+
calls.push('cb1')
102+
}
103+
const cb2 = () => {
104+
calls.push('cb2')
105+
}
106+
const cb3 = () => {
107+
calls.push('cb3')
108+
}
109+
110+
queuePreFlushCb(cb1)
111+
queuePreFlushCb(cb2)
112+
queuePreFlushCb(cb1)
113+
queuePreFlushCb(cb2)
114+
queuePreFlushCb(cb3)
115+
116+
expect(calls).toEqual([])
117+
await nextTick()
118+
expect(calls).toEqual(['cb1', 'cb2', 'cb3'])
119+
})
120+
121+
it('chained queuePreFlushCb', async () => {
122+
const calls: string[] = []
123+
const cb1 = () => {
124+
calls.push('cb1')
125+
// cb2 will be executed after cb1 at the same tick
126+
queuePreFlushCb(cb2)
127+
}
128+
const cb2 = () => {
129+
calls.push('cb2')
130+
}
131+
queuePreFlushCb(cb1)
132+
133+
await nextTick()
134+
expect(calls).toEqual(['cb1', 'cb2'])
135+
})
136+
})
137+
138+
describe('queueJob w/ queuePreFlushCb', () => {
139+
it('queueJob inside preFlushCb', async () => {
140+
const calls: string[] = []
141+
const job1 = () => {
142+
calls.push('job1')
143+
}
144+
const cb1 = () => {
145+
// queueJob in postFlushCb
146+
calls.push('cb1')
147+
queueJob(job1)
148+
}
149+
150+
queuePreFlushCb(cb1)
151+
await nextTick()
152+
expect(calls).toEqual(['cb1', 'job1'])
153+
})
154+
155+
it('queueJob & preFlushCb inside preFlushCb', async () => {
156+
const calls: string[] = []
157+
const job1 = () => {
158+
calls.push('job1')
159+
}
160+
const cb1 = () => {
161+
calls.push('cb1')
162+
queueJob(job1)
163+
// cb2 should execute before the job
164+
queuePreFlushCb(cb2)
165+
}
166+
const cb2 = () => {
167+
calls.push('cb2')
168+
}
169+
170+
queuePreFlushCb(cb1)
171+
await nextTick()
172+
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
173+
})
174+
175+
it('preFlushCb inside queueJob', async () => {
176+
const calls: string[] = []
177+
const job1 = () => {
178+
// the only case where a pre-flush cb can be queued inside a job is
179+
// when updating the props of a child component. This is handled
180+
// directly inside `updateComponentPreRender` to avoid non atomic
181+
// cb triggers (#1763)
182+
queuePreFlushCb(cb1)
183+
queuePreFlushCb(cb2)
184+
flushPreFlushCbs(undefined, job1)
185+
calls.push('job1')
186+
}
187+
const cb1 = () => {
188+
calls.push('cb1')
189+
// a cb triggers its parent job, which should be skipped
190+
queueJob(job1)
191+
}
192+
const cb2 = () => {
193+
calls.push('cb2')
194+
}
195+
196+
queueJob(job1)
197+
await nextTick()
198+
expect(calls).toEqual(['cb1', 'cb2', 'job1'])
199+
})
200+
})
201+
78202
describe('queuePostFlushCb', () => {
79203
it('basic usage', async () => {
80204
const calls: string[] = []

packages/runtime-core/src/apiWatch.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import {
77
ReactiveEffectOptions,
88
isReactive
99
} from '@vue/reactivity'
10-
import { queueJob, SchedulerJob } from './scheduler'
10+
import { SchedulerJob, queuePreFlushCb } from './scheduler'
1111
import {
1212
EMPTY_OBJ,
1313
isObject,
@@ -271,7 +271,7 @@ function doWatch(
271271
job.id = -1
272272
scheduler = () => {
273273
if (!instance || instance.isMounted) {
274-
queueJob(job)
274+
queuePreFlushCb(job)
275275
} else {
276276
// with 'pre' option, the first call must happen before
277277
// the component is mounted so it is called synchronously.

packages/runtime-core/src/renderer.ts

+5-2
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ import {
4141
queuePostFlushCb,
4242
flushPostFlushCbs,
4343
invalidateJob,
44-
runPreflushJobs
44+
flushPreFlushCbs
4545
} from './scheduler'
4646
import { effect, stop, ReactiveEffectOptions, isRef } from '@vue/reactivity'
4747
import { updateProps } from './componentProps'
@@ -1430,7 +1430,10 @@ function baseCreateRenderer(
14301430
instance.next = null
14311431
updateProps(instance, nextVNode.props, prevProps, optimized)
14321432
updateSlots(instance, nextVNode.children)
1433-
runPreflushJobs(instance.update)
1433+
1434+
// props update may have triggered pre-flush watchers.
1435+
// flush them before the render update.
1436+
flushPreFlushCbs(undefined, instance.update)
14341437
}
14351438

14361439
const patchChildren: PatchChildrenFn = (

packages/runtime-core/src/scheduler.ts

+78-57
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,23 @@ export interface SchedulerJob {
1616
cb?: boolean
1717
}
1818

19+
let isFlushing = false
20+
let isFlushPending = false
21+
1922
const queue: (SchedulerJob | null)[] = []
20-
const postFlushCbs: Function[] = []
23+
let flushIndex = 0
24+
25+
const pendingPreFlushCbs: Function[] = []
26+
let activePreFlushCbs: Function[] | null = null
27+
let preFlushIndex = 0
28+
29+
const pendingPostFlushCbs: Function[] = []
30+
let activePostFlushCbs: Function[] | null = null
31+
let postFlushIndex = 0
32+
2133
const resolvedPromise: Promise<any> = Promise.resolve()
2234
let currentFlushPromise: Promise<void> | null = null
2335

24-
let isFlushing = false
25-
let isFlushPending = false
26-
let flushIndex = 0
27-
let pendingPostFlushCbs: Function[] | null = null
28-
let pendingPostFlushIndex = 0
29-
let hasPendingPreFlushJobs = false
3036
let currentPreFlushParentJob: SchedulerJob | null = null
3137

3238
const RECURSION_LIMIT = 100
@@ -53,90 +59,102 @@ export function queueJob(job: SchedulerJob) {
5359
job !== currentPreFlushParentJob
5460
) {
5561
queue.push(job)
56-
if ((job.id as number) < 0) hasPendingPreFlushJobs = true
5762
queueFlush()
5863
}
5964
}
6065

66+
function queueFlush() {
67+
if (!isFlushing && !isFlushPending) {
68+
isFlushPending = true
69+
currentFlushPromise = resolvedPromise.then(flushJobs)
70+
}
71+
}
72+
6173
export function invalidateJob(job: SchedulerJob) {
6274
const i = queue.indexOf(job)
6375
if (i > -1) {
6476
queue[i] = null
6577
}
6678
}
6779

68-
/**
69-
* Run flush: 'pre' watcher callbacks. This is only called in
70-
* `updateComponentPreRender` to cover the case where pre-flush watchers are
71-
* triggered by the change of a component's props. This means the scheduler is
72-
* already flushing and we are already inside the component's update effect,
73-
* right when the render function is about to be called. So if the watcher
74-
* triggers the same component to update, we don't want it to be queued (this
75-
* is checked via `currentPreFlushParentJob`).
76-
*/
77-
export function runPreflushJobs(parentJob: SchedulerJob) {
78-
if (hasPendingPreFlushJobs) {
79-
currentPreFlushParentJob = parentJob
80-
hasPendingPreFlushJobs = false
81-
for (let job, i = flushIndex + 1; i < queue.length; i++) {
82-
job = queue[i]
83-
if (job && (job.id as number) < 0) {
84-
job()
85-
queue[i] = null
86-
}
87-
}
88-
currentPreFlushParentJob = null
89-
}
90-
}
91-
92-
export function queuePostFlushCb(cb: Function | Function[]) {
80+
function queueCb(
81+
cb: Function | Function[],
82+
activeQueue: Function[] | null,
83+
pendingQueue: Function[],
84+
index: number
85+
) {
9386
if (!isArray(cb)) {
9487
if (
95-
!pendingPostFlushCbs ||
96-
!pendingPostFlushCbs.includes(
97-
cb,
98-
(cb as SchedulerJob).cb
99-
? pendingPostFlushIndex + 1
100-
: pendingPostFlushIndex
101-
)
88+
!activeQueue ||
89+
!activeQueue.includes(cb, (cb as SchedulerJob).cb ? index + 1 : index)
10290
) {
103-
postFlushCbs.push(cb)
91+
pendingQueue.push(cb)
10492
}
10593
} else {
10694
// if cb is an array, it is a component lifecycle hook which can only be
10795
// triggered by a job, which is already deduped in the main queue, so
10896
// we can skip dupicate check here to improve perf
109-
postFlushCbs.push(...cb)
97+
pendingQueue.push(...cb)
11098
}
11199
queueFlush()
112100
}
113101

114-
function queueFlush() {
115-
if (!isFlushing && !isFlushPending) {
116-
isFlushPending = true
117-
currentFlushPromise = resolvedPromise.then(flushJobs)
102+
export function queuePreFlushCb(cb: Function) {
103+
queueCb(cb, activePreFlushCbs, pendingPreFlushCbs, preFlushIndex)
104+
}
105+
106+
export function queuePostFlushCb(cb: Function | Function[]) {
107+
queueCb(cb, activePostFlushCbs, pendingPostFlushCbs, postFlushIndex)
108+
}
109+
110+
export function flushPreFlushCbs(
111+
seen?: CountMap,
112+
parentJob: SchedulerJob | null = null
113+
) {
114+
if (pendingPreFlushCbs.length) {
115+
currentPreFlushParentJob = parentJob
116+
activePreFlushCbs = [...new Set(pendingPreFlushCbs)]
117+
pendingPreFlushCbs.length = 0
118+
if (__DEV__) {
119+
seen = seen || new Map()
120+
}
121+
for (
122+
preFlushIndex = 0;
123+
preFlushIndex < activePreFlushCbs.length;
124+
preFlushIndex++
125+
) {
126+
if (__DEV__) {
127+
checkRecursiveUpdates(seen!, activePreFlushCbs[preFlushIndex])
128+
}
129+
activePreFlushCbs[preFlushIndex]()
130+
}
131+
activePreFlushCbs = null
132+
preFlushIndex = 0
133+
currentPreFlushParentJob = null
134+
// recursively flush until it drains
135+
flushPreFlushCbs(seen, parentJob)
118136
}
119137
}
120138

121139
export function flushPostFlushCbs(seen?: CountMap) {
122-
if (postFlushCbs.length) {
123-
pendingPostFlushCbs = [...new Set(postFlushCbs)]
124-
postFlushCbs.length = 0
140+
if (pendingPostFlushCbs.length) {
141+
activePostFlushCbs = [...new Set(pendingPostFlushCbs)]
142+
pendingPostFlushCbs.length = 0
125143
if (__DEV__) {
126144
seen = seen || new Map()
127145
}
128146
for (
129-
pendingPostFlushIndex = 0;
130-
pendingPostFlushIndex < pendingPostFlushCbs.length;
131-
pendingPostFlushIndex++
147+
postFlushIndex = 0;
148+
postFlushIndex < activePostFlushCbs.length;
149+
postFlushIndex++
132150
) {
133151
if (__DEV__) {
134-
checkRecursiveUpdates(seen!, pendingPostFlushCbs[pendingPostFlushIndex])
152+
checkRecursiveUpdates(seen!, activePostFlushCbs[postFlushIndex])
135153
}
136-
pendingPostFlushCbs[pendingPostFlushIndex]()
154+
activePostFlushCbs[postFlushIndex]()
137155
}
138-
pendingPostFlushCbs = null
139-
pendingPostFlushIndex = 0
156+
activePostFlushCbs = null
157+
postFlushIndex = 0
140158
}
141159
}
142160

@@ -149,6 +167,8 @@ function flushJobs(seen?: CountMap) {
149167
seen = seen || new Map()
150168
}
151169

170+
flushPreFlushCbs(seen)
171+
152172
// Sort queue before flush.
153173
// This ensures that:
154174
// 1. Components are updated from parent to child. (because parent is always
@@ -175,11 +195,12 @@ function flushJobs(seen?: CountMap) {
175195
queue.length = 0
176196

177197
flushPostFlushCbs(seen)
198+
178199
isFlushing = false
179200
currentFlushPromise = null
180201
// some postFlushCb queued jobs!
181202
// keep flushing until it drains.
182-
if (queue.length || postFlushCbs.length) {
203+
if (queue.length || pendingPostFlushCbs.length) {
183204
flushJobs(seen)
184205
}
185206
}

0 commit comments

Comments
 (0)