Skip to content

Commit

Permalink
refactor(scheduler): use bitwise flags for scheduler jobs + move sche…
Browse files Browse the repository at this point in the history
…duler into reactivity

related: vuejs/core#10407
  • Loading branch information
LittleSound committed Mar 17, 2024
1 parent 174118a commit db4040d
Show file tree
Hide file tree
Showing 8 changed files with 142 additions and 125 deletions.
19 changes: 14 additions & 5 deletions packages/reactivity/__tests__/baseWatch.spec.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Scheduler, SchedulerJob } from '../src/baseWatch'
import {
BaseWatchErrorCodes,
EffectScope,
type Ref,
type SchedulerJob,
type WatchScheduler,
baseWatch,
onEffectCleanup,
ref,
Expand All @@ -15,9 +16,13 @@ let isFlushPending = false
const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise<any>
const nextTick = (fn?: () => any) =>
fn ? resolvedPromise.then(fn) : resolvedPromise
const scheduler: Scheduler = job => {
queue.push(job)
flushJobs()
const scheduler: WatchScheduler = (job, effect, immediateFirstRun, hasCb) => {
if (immediateFirstRun) {
!hasCb && effect.run()
} else {
queue.push(() => job(immediateFirstRun))
flushJobs()
}
}
const flushJobs = () => {
if (isFlushPending) return
Expand Down Expand Up @@ -214,7 +219,11 @@ describe('baseWatch', () => {
},
)

expect(effectCalls).toEqual([])
expect(effectCalls).toEqual([
'before effect running',
'effect',
'effect ran',
])
expect(watchCalls).toEqual([])
await nextTick()
expect(effectCalls).toEqual([
Expand Down
47 changes: 12 additions & 35 deletions packages/reactivity/src/baseWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ import {
} from './effect'
import { isReactive, isShallow } from './reactive'
import { type Ref, isRef } from './ref'
import { getCurrentScope } from './effectScope'
import { type SchedulerJob, SchedulerJobFlags } from './scheduler'

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

// TODO move to a scheduler package
export interface SchedulerJob extends Function {
id?: number
// TODO refactor these boolean flags to a single bitwise flag
pre?: boolean
active?: boolean
computed?: boolean
queued?: boolean
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
*
* By default, a job cannot trigger itself because some built-in method calls,
* e.g. Array.prototype.push actually performs reads as well (#1740) which
* can lead to confusing infinite loops.
* The allowed cases are component update functions and watch callbacks.
* Component update functions may update child component props, which in turn
* trigger flush: "pre" watch callbacks that mutates state that the parent
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
* triggers itself again, it's likely intentional and it is the user's
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
allowRecurse?: boolean
}

type WatchEffect = (onCleanup: OnCleanup) => void
type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
type WatchCallback<V = any, OV = any> = (
Expand Down Expand Up @@ -254,8 +228,11 @@ export function baseWatch(
let oldValue: any = isMultiSource
? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE)
: INITIAL_WATCHER_VALUE
const job: SchedulerJob = () => {
if (!effect.active || !effect.dirty) {
const job: SchedulerJob = (immediateFirstRun?: boolean) => {
if (
!(effect.flags & EffectFlags.ACTIVE) ||
(!effect.dirty && !immediateFirstRun)
) {
return
}
if (cb) {
Expand Down Expand Up @@ -310,11 +287,10 @@ export function baseWatch(

// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb

let effectScheduler: EffectScheduler = () => scheduler(job, effect, false)
if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE

effect = new ReactiveEffect(getter, NOOP, effectScheduler, scope)
effect = new ReactiveEffect(getter)
effect.scheduler = () => scheduler(job, effect, false, !!cb)

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

// initial run
if (cb) {
scheduler(job, effect, true, !!cb)
if (immediate) {
job()
job(true)
} else {
oldValue = effect.run()
}
} else {
scheduler(job, effect, true)
scheduler(job, effect, true, !!cb)
}

return effect
Expand Down
2 changes: 1 addition & 1 deletion packages/runtime-core/__tests__/scheduler.spec.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { SchedulerJobFlags } from '@vue/reactivity'
import {
type SchedulerJob,
SchedulerJobFlags,
flushPostFlushCbs,
flushPreFlushCbs,
invalidateJob,
Expand Down
3 changes: 1 addition & 2 deletions packages/runtime-core/src/components/BaseTransition.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,11 @@ import {
} from '../vnode'
import { warn } from '../warning'
import { isKeepAlive } from './KeepAlive'
import { toRaw } from '@vue/reactivity'
import { SchedulerJobFlags, toRaw } from '@vue/reactivity'
import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling'
import { PatchFlags, ShapeFlags, isArray } from '@vue/shared'
import { onBeforeUnmount, onMounted } from '../apiLifecycle'
import type { RendererElement } from '../renderer'
import { SchedulerJobFlags } from '../scheduler'

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

Expand Down
10 changes: 5 additions & 5 deletions packages/runtime-core/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@ import {
import {
type SchedulerFactory,
type SchedulerJob,
SchedulerJobFlags,
flushPostFlushCbs,
flushPreFlushCbs,
invalidateJob,
Expand All @@ -50,6 +49,7 @@ import {
import {
EffectFlags,
ReactiveEffect,
SchedulerJobFlags,
pauseTracking,
resetTracking,
} from '@vue/reactivity'
Expand Down Expand Up @@ -289,14 +289,14 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__
: queuePostFlushCb

export const createPostRenderScheduler: SchedulerFactory =
instance => (job, effect, isInit) => {
if (isInit) {
instance => (job, effect, immediateFirstRun, hasCb) => {
if (!immediateFirstRun) {
queuePostRenderEffect(job, instance && instance.suspense)
} else if (!hasCb) {
queuePostRenderEffect(
effect.run.bind(effect),
instance && instance.suspense,
)
} else {
queuePostRenderEffect(job, instance && instance.suspense)
}
}

Expand Down
58 changes: 18 additions & 40 deletions packages/runtime-core/src/scheduler.ts
Original file line number Diff line number Diff line change
@@ -1,37 +1,14 @@
import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
import { type Awaited, NOOP, isArray } from '@vue/shared'
import { type ComponentInternalInstance, getComponentName } from './component'
import type { Scheduler } from '@vue/reactivity'

export enum SchedulerJobFlags {
QUEUED = 1 << 0,
PRE = 1 << 1,
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
*
* By default, a job cannot trigger itself because some built-in method calls,
* e.g. Array.prototype.push actually performs reads as well (#1740) which
* can lead to confusing infinite loops.
* The allowed cases are component update functions and watch callbacks.
* Component update functions may update child component props, which in turn
* trigger flush: "pre" watch callbacks that mutates state that the parent
* relies on (#1801). Watch callbacks doesn't track its dependencies so if it
* triggers itself again, it's likely intentional and it is the user's
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
ALLOW_RECURSE = 1 << 2,
DISPOSED = 1 << 3,
}

export interface SchedulerJob extends Function {
id?: number
/**
* flags can technically be undefined, but it can still be used in bitwise
* operations just like 0.
*/
flags?: SchedulerJobFlags
import {
type SchedulerJob as BaseSchedulerJob,
EffectFlags,
SchedulerJobFlags,
type WatchScheduler,
} from '@vue/reactivity'

export interface SchedulerJob extends BaseSchedulerJob {
/**
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
Expand Down Expand Up @@ -301,24 +278,25 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {

export type SchedulerFactory = (
instance: ComponentInternalInstance | null,
) => Scheduler
) => WatchScheduler

export const createSyncScheduler: SchedulerFactory =
instance => (job, effect, isInit) => {
if (isInit) {
effect.run()
instance => (job, effect, immediateFirstRun, hasCb) => {
if (immediateFirstRun) {
effect.flags |= EffectFlags.NO_BATCH
if (!hasCb) effect.run()
} else {
job()
}
}

export const createPreScheduler: SchedulerFactory =
instance => (job, effect, isInit) => {
if (isInit) {
effect.run()
} else {
job.pre = true
instance => (job, effect, immediateFirstRun, hasCb) => {
if (!immediateFirstRun) {
job.flags! |= SchedulerJobFlags.PRE
if (instance) job.id = instance.uid
queueJob(job)
} else if (!hasCb) {
effect.run()
}
}
42 changes: 42 additions & 0 deletions packages/runtime-vapor/__tests__/renderWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,11 +38,24 @@ describe('renderWatch', () => {
renderEffect(() => {
dummy = source.value
})
expect(dummy).toBe(0)
await nextTick()
expect(dummy).toBe(0)

source.value++
expect(dummy).toBe(0)
await nextTick()
expect(dummy).toBe(1)

source.value++
expect(dummy).toBe(1)
await nextTick()
expect(dummy).toBe(2)

source.value++
expect(dummy).toBe(2)
await nextTick()
expect(dummy).toBe(3)
})

test('watch', async () => {
Expand All @@ -53,9 +66,16 @@ describe('renderWatch', () => {
})
await nextTick()
expect(dummy).toBe(undefined)

source.value++
expect(dummy).toBe(undefined)
await nextTick()
expect(dummy).toBe(1)

source.value++
expect(dummy).toBe(1)
await nextTick()
expect(dummy).toBe(2)
})

test('should run with the scheduling order', async () => {
Expand Down Expand Up @@ -136,6 +156,28 @@ describe('renderWatch', () => {
'post 1',
'updated 1',
])
calls.length = 0

// Update
changeRender()
change()

expect(calls).toEqual(['sync cleanup 1', 'sync 2'])
calls.length = 0

await nextTick()
expect(calls).toEqual([
'pre cleanup 1',
'pre 2',
'beforeUpdate 2',
'renderEffect cleanup 1',
'renderEffect 2',
'renderWatch cleanup 1',
'renderWatch 2',
'post cleanup 1',
'post 2',
'updated 2',
])
})

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

0 comments on commit db4040d

Please sign in to comment.