Skip to content

Commit db4040d

Browse files
committed
refactor(scheduler): use bitwise flags for scheduler jobs + move scheduler into reactivity
related: vuejs/core#10407
1 parent 174118a commit db4040d

File tree

8 files changed

+142
-125
lines changed

8 files changed

+142
-125
lines changed

packages/reactivity/__tests__/baseWatch.spec.ts

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
import type { Scheduler, SchedulerJob } from '../src/baseWatch'
21
import {
32
BaseWatchErrorCodes,
43
EffectScope,
54
type Ref,
5+
type SchedulerJob,
6+
type WatchScheduler,
67
baseWatch,
78
onEffectCleanup,
89
ref,
@@ -15,9 +16,13 @@ let isFlushPending = false
1516
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
1617
const nextTick = (fn?: () => any) =>
1718
fn ? resolvedPromise.then(fn) : resolvedPromise
18-
const scheduler: Scheduler = job => {
19-
queue.push(job)
20-
flushJobs()
19+
const scheduler: WatchScheduler = (job, effect, immediateFirstRun, hasCb) => {
20+
if (immediateFirstRun) {
21+
!hasCb && effect.run()
22+
} else {
23+
queue.push(() => job(immediateFirstRun))
24+
flushJobs()
25+
}
2126
}
2227
const flushJobs = () => {
2328
if (isFlushPending) return
@@ -214,7 +219,11 @@ describe('baseWatch', () => {
214219
},
215220
)
216221

217-
expect(effectCalls).toEqual([])
222+
expect(effectCalls).toEqual([
223+
'before effect running',
224+
'effect',
225+
'effect ran',
226+
])
218227
expect(watchCalls).toEqual([])
219228
await nextTick()
220229
expect(effectCalls).toEqual([

packages/reactivity/src/baseWatch.ts

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ import {
2222
} from './effect'
2323
import { isReactive, isShallow } from './reactive'
2424
import { type Ref, isRef } from './ref'
25-
import { getCurrentScope } from './effectScope'
25+
import { type SchedulerJob, SchedulerJobFlags } from './scheduler'
2626

2727
// These errors were transferred from `packages/runtime-core/src/errorHandling.ts`
2828
// along with baseWatch to maintain code compatibility. Hence,
@@ -33,32 +33,6 @@ export enum BaseWatchErrorCodes {
3333
WATCH_CLEANUP,
3434
}
3535

36-
// TODO move to a scheduler package
37-
export interface SchedulerJob extends Function {
38-
id?: number
39-
// TODO refactor these boolean flags to a single bitwise flag
40-
pre?: boolean
41-
active?: boolean
42-
computed?: boolean
43-
queued?: boolean
44-
/**
45-
* Indicates whether the effect is allowed to recursively trigger itself
46-
* when managed by the scheduler.
47-
*
48-
* By default, a job cannot trigger itself because some built-in method calls,
49-
* e.g. Array.prototype.push actually performs reads as well (#1740) which
50-
* can lead to confusing infinite loops.
51-
* The allowed cases are component update functions and watch callbacks.
52-
* Component update functions may update child component props, which in turn
53-
* trigger flush: "pre" watch callbacks that mutates state that the parent
54-
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
55-
* triggers itself again, it's likely intentional and it is the user's
56-
* responsibility to perform recursive state mutation that eventually
57-
* stabilizes (#1727).
58-
*/
59-
allowRecurse?: boolean
60-
}
61-
6236
type WatchEffect = (onCleanup: OnCleanup) => void
6337
type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
6438
type WatchCallback<V = any, OV = any> = (
@@ -254,8 +228,11 @@ export function baseWatch(
254228
let oldValue: any = isMultiSource
255229
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
256230
: INITIAL_WATCHER_VALUE
257-
const job: SchedulerJob = () => {
258-
if (!effect.active || !effect.dirty) {
231+
const job: SchedulerJob = (immediateFirstRun?: boolean) => {
232+
if (
233+
!(effect.flags & EffectFlags.ACTIVE) ||
234+
(!effect.dirty && !immediateFirstRun)
235+
) {
259236
return
260237
}
261238
if (cb) {
@@ -310,11 +287,10 @@ export function baseWatch(
310287

311288
// important: mark the job as a watcher callback so that scheduler knows
312289
// it is allowed to self-trigger (#1727)
313-
job.allowRecurse = !!cb
314-
315-
let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
290+
if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
316291

317-
effect = new ReactiveEffect(getter, NOOP, effectScheduler, scope)
292+
effect = new ReactiveEffect(getter)
293+
effect.scheduler = () => scheduler(job, effect, false, !!cb)
318294

319295
cleanup = effect.onStop = () => {
320296
const cleanups = cleanupMap.get(effect)
@@ -337,13 +313,14 @@ export function baseWatch(
337313

338314
// initial run
339315
if (cb) {
316+
scheduler(job, effect, true, !!cb)
340317
if (immediate) {
341-
job()
318+
job(true)
342319
} else {
343320
oldValue = effect.run()
344321
}
345322
} else {
346-
scheduler(job, effect, true)
323+
scheduler(job, effect, true, !!cb)
347324
}
348325

349326
return effect

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1+
import { SchedulerJobFlags } from '@vue/reactivity'
12
import {
23
type SchedulerJob,
3-
SchedulerJobFlags,
44
flushPostFlushCbs,
55
flushPreFlushCbs,
66
invalidateJob,

packages/runtime-core/src/components/BaseTransition.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,11 @@ import {
1414
} from '../vnode'
1515
import { warn } from '../warning'
1616
import { isKeepAlive } from './KeepAlive'
17-
import { toRaw } from '@vue/reactivity'
17+
import { SchedulerJobFlags, toRaw } from '@vue/reactivity'
1818
import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
1919
import { PatchFlags, ShapeFlags, isArray } from '@vue/shared'
2020
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
2121
import type { RendererElement } from '../renderer'
22-
import { SchedulerJobFlags } from '../scheduler'
2322

2423
type Hook<T = () => void> = T | T[]
2524

packages/runtime-core/src/renderer.ts

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,6 @@ import {
4040
import {
4141
type SchedulerFactory,
4242
type SchedulerJob,
43-
SchedulerJobFlags,
4443
flushPostFlushCbs,
4544
flushPreFlushCbs,
4645
invalidateJob,
@@ -50,6 +49,7 @@ import {
5049
import {
5150
EffectFlags,
5251
ReactiveEffect,
52+
SchedulerJobFlags,
5353
pauseTracking,
5454
resetTracking,
5555
} from '@vue/reactivity'
@@ -289,14 +289,14 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
289289
: queuePostFlushCb
290290

291291
export const createPostRenderScheduler: SchedulerFactory =
292-
instance => (job, effect, isInit) => {
293-
if (isInit) {
292+
instance => (job, effect, immediateFirstRun, hasCb) => {
293+
if (!immediateFirstRun) {
294+
queuePostRenderEffect(job, instance && instance.suspense)
295+
} else if (!hasCb) {
294296
queuePostRenderEffect(
295297
effect.run.bind(effect),
296298
instance && instance.suspense,
297299
)
298-
} else {
299-
queuePostRenderEffect(job, instance && instance.suspense)
300300
}
301301
}
302302

packages/runtime-core/src/scheduler.ts

Lines changed: 18 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,14 @@
11
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
22
import { type Awaited, NOOP, isArray } from '@vue/shared'
33
import { type ComponentInternalInstance, getComponentName } from './component'
4-
import type { Scheduler } from '@vue/reactivity'
5-
6-
export enum SchedulerJobFlags {
7-
QUEUED = 1 << 0,
8-
PRE = 1 << 1,
9-
/**
10-
* Indicates whether the effect is allowed to recursively trigger itself
11-
* when managed by the scheduler.
12-
*
13-
* By default, a job cannot trigger itself because some built-in method calls,
14-
* e.g. Array.prototype.push actually performs reads as well (#1740) which
15-
* can lead to confusing infinite loops.
16-
* The allowed cases are component update functions and watch callbacks.
17-
* Component update functions may update child component props, which in turn
18-
* trigger flush: "pre" watch callbacks that mutates state that the parent
19-
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
20-
* triggers itself again, it's likely intentional and it is the user's
21-
* responsibility to perform recursive state mutation that eventually
22-
* stabilizes (#1727).
23-
*/
24-
ALLOW_RECURSE = 1 << 2,
25-
DISPOSED = 1 << 3,
26-
}
27-
28-
export interface SchedulerJob extends Function {
29-
id?: number
30-
/**
31-
* flags can technically be undefined, but it can still be used in bitwise
32-
* operations just like 0.
33-
*/
34-
flags?: SchedulerJobFlags
4+
import {
5+
type SchedulerJob as BaseSchedulerJob,
6+
EffectFlags,
7+
SchedulerJobFlags,
8+
type WatchScheduler,
9+
} from '@vue/reactivity'
10+
11+
export interface SchedulerJob extends BaseSchedulerJob {
3512
/**
3613
* Attached by renderer.ts when setting up a component's render effect
3714
* Used to obtain component information when reporting max recursive updates.
@@ -301,24 +278,25 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
301278

302279
export type SchedulerFactory = (
303280
instance: ComponentInternalInstance | null,
304-
) => Scheduler
281+
) => WatchScheduler
305282

306283
export const createSyncScheduler: SchedulerFactory =
307-
instance => (job, effect, isInit) => {
308-
if (isInit) {
309-
effect.run()
284+
instance => (job, effect, immediateFirstRun, hasCb) => {
285+
if (immediateFirstRun) {
286+
effect.flags |= EffectFlags.NO_BATCH
287+
if (!hasCb) effect.run()
310288
} else {
311289
job()
312290
}
313291
}
314292

315293
export const createPreScheduler: SchedulerFactory =
316-
instance => (job, effect, isInit) => {
317-
if (isInit) {
318-
effect.run()
319-
} else {
320-
job.pre = true
294+
instance => (job, effect, immediateFirstRun, hasCb) => {
295+
if (!immediateFirstRun) {
296+
job.flags! |= SchedulerJobFlags.PRE
321297
if (instance) job.id = instance.uid
322298
queueJob(job)
299+
} else if (!hasCb) {
300+
effect.run()
323301
}
324302
}

packages/runtime-vapor/__tests__/renderWatch.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,11 +38,24 @@ describe('renderWatch', () => {
3838
renderEffect(() => {
3939
dummy = source.value
4040
})
41+
expect(dummy).toBe(0)
4142
await nextTick()
4243
expect(dummy).toBe(0)
44+
4345
source.value++
46+
expect(dummy).toBe(0)
4447
await nextTick()
4548
expect(dummy).toBe(1)
49+
50+
source.value++
51+
expect(dummy).toBe(1)
52+
await nextTick()
53+
expect(dummy).toBe(2)
54+
55+
source.value++
56+
expect(dummy).toBe(2)
57+
await nextTick()
58+
expect(dummy).toBe(3)
4659
})
4760

4861
test('watch', async () => {
@@ -53,9 +66,16 @@ describe('renderWatch', () => {
5366
})
5467
await nextTick()
5568
expect(dummy).toBe(undefined)
69+
5670
source.value++
71+
expect(dummy).toBe(undefined)
5772
await nextTick()
5873
expect(dummy).toBe(1)
74+
75+
source.value++
76+
expect(dummy).toBe(1)
77+
await nextTick()
78+
expect(dummy).toBe(2)
5979
})
6080

6181
test('should run with the scheduling order', async () => {
@@ -136,6 +156,28 @@ describe('renderWatch', () => {
136156
'post 1',
137157
'updated 1',
138158
])
159+
calls.length = 0
160+
161+
// Update
162+
changeRender()
163+
change()
164+
165+
expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
166+
calls.length = 0
167+
168+
await nextTick()
169+
expect(calls).toEqual([
170+
'pre cleanup 1',
171+
'pre 2',
172+
'beforeUpdate 2',
173+
'renderEffect cleanup 1',
174+
'renderEffect 2',
175+
'renderWatch cleanup 1',
176+
'renderWatch 2',
177+
'post cleanup 1',
178+
'post 2',
179+
'updated 2',
180+
])
139181
})
140182

141183
test('errors should include the execution location with beforeUpdate hook', async () => {

0 commit comments

Comments
 (0)