Skip to content

Commit 78977c3

Browse files
committed
fix(scheduler): sort jobs before flushing
This fixes the case where a child component is added to the queue before its parent, but should be invalidated by its parent's update. Same logic was present in Vue 2. Properly fixes #910 ref: #910 (comment)
1 parent c80b857 commit 78977c3

File tree

3 files changed

+46
-9
lines changed

3 files changed

+46
-9
lines changed

packages/reactivity/src/effect.ts

+5-1
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const targetMap = new WeakMap<any, KeyToDepMap>()
1212
export interface ReactiveEffect<T = any> {
1313
(...args: any[]): T
1414
_isEffect: true
15+
id: number
1516
active: boolean
1617
raw: () => T
1718
deps: Array<Dep>
@@ -21,7 +22,7 @@ export interface ReactiveEffect<T = any> {
2122
export interface ReactiveEffectOptions {
2223
lazy?: boolean
2324
computed?: boolean
24-
scheduler?: (job: () => void) => void
25+
scheduler?: (job: ReactiveEffect) => void
2526
onTrack?: (event: DebuggerEvent) => void
2627
onTrigger?: (event: DebuggerEvent) => void
2728
onStop?: () => void
@@ -74,6 +75,8 @@ export function stop(effect: ReactiveEffect) {
7475
}
7576
}
7677

78+
let uid = 0
79+
7780
function createReactiveEffect<T = any>(
7881
fn: (...args: any[]) => T,
7982
options: ReactiveEffectOptions
@@ -96,6 +99,7 @@ function createReactiveEffect<T = any>(
9699
}
97100
}
98101
} as ReactiveEffect
102+
effect.id = uid++
99103
effect._isEffect = true
100104
effect.active = true
101105
effect.raw = fn

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

+16
Original file line numberDiff line numberDiff line change
@@ -262,4 +262,20 @@ describe('scheduler', () => {
262262
// job2 should be called only once
263263
expect(calls).toEqual(['job1', 'job2', 'job3', 'job4'])
264264
})
265+
266+
test('sort job based on id', async () => {
267+
const calls: string[] = []
268+
const job1 = () => calls.push('job1')
269+
// job1 has no id
270+
const job2 = () => calls.push('job2')
271+
job2.id = 2
272+
const job3 = () => calls.push('job3')
273+
job3.id = 1
274+
275+
queueJob(job1)
276+
queueJob(job2)
277+
queueJob(job3)
278+
await nextTick()
279+
expect(calls).toEqual(['job3', 'job2', 'job1'])
280+
})
265281
})

packages/runtime-core/src/scheduler.ts

+25-8
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,33 @@
11
import { ErrorCodes, callWithErrorHandling } from './errorHandling'
22
import { isArray } from '@vue/shared'
33

4-
const queue: (Function | null)[] = []
4+
export interface Job {
5+
(): void
6+
id?: number
7+
}
8+
9+
const queue: (Job | null)[] = []
510
const postFlushCbs: Function[] = []
611
const p = Promise.resolve()
712

813
let isFlushing = false
914
let isFlushPending = false
1015

1116
const RECURSION_LIMIT = 100
12-
type CountMap = Map<Function, number>
17+
type CountMap = Map<Job | Function, number>
1318

1419
export function nextTick(fn?: () => void): Promise<void> {
1520
return fn ? p.then(fn) : p
1621
}
1722

18-
export function queueJob(job: () => void) {
23+
export function queueJob(job: Job) {
1924
if (!queue.includes(job)) {
2025
queue.push(job)
2126
queueFlush()
2227
}
2328
}
2429

25-
export function invalidateJob(job: () => void) {
30+
export function invalidateJob(job: Job) {
2631
const i = queue.indexOf(job)
2732
if (i > -1) {
2833
queue[i] = null
@@ -45,11 +50,9 @@ function queueFlush() {
4550
}
4651
}
4752

48-
const dedupe = (cbs: Function[]): Function[] => [...new Set(cbs)]
49-
5053
export function flushPostFlushCbs(seen?: CountMap) {
5154
if (postFlushCbs.length) {
52-
const cbs = dedupe(postFlushCbs)
55+
const cbs = [...new Set(postFlushCbs)]
5356
postFlushCbs.length = 0
5457
if (__DEV__) {
5558
seen = seen || new Map()
@@ -63,13 +66,27 @@ export function flushPostFlushCbs(seen?: CountMap) {
6366
}
6467
}
6568

69+
const getId = (job: Job) => (job.id == null ? Infinity : job.id)
70+
6671
function flushJobs(seen?: CountMap) {
6772
isFlushPending = false
6873
isFlushing = true
6974
let job
7075
if (__DEV__) {
7176
seen = seen || new Map()
7277
}
78+
79+
// Sort queue before flush.
80+
// This ensures that:
81+
// 1. Components are updated from parent to child. (because parent is always
82+
// created before the child so its render effect will have smaller
83+
// priority number)
84+
// 2. If a component is unmounted during a parent component's update,
85+
// its update can be skipped.
86+
// Jobs can never be null before flush starts, since they are only invalidated
87+
// during execution of another flushed job.
88+
queue.sort((a, b) => getId(a!) - getId(b!))
89+
7390
while ((job = queue.shift()) !== undefined) {
7491
if (job === null) {
7592
continue
@@ -88,7 +105,7 @@ function flushJobs(seen?: CountMap) {
88105
}
89106
}
90107

91-
function checkRecursiveUpdates(seen: CountMap, fn: Function) {
108+
function checkRecursiveUpdates(seen: CountMap, fn: Job | Function) {
92109
if (!seen.has(fn)) {
93110
seen.set(fn, 1)
94111
} else {

0 commit comments

Comments
 (0)