refactor(scheduler): use bitwise flags for scheduler jobs + optimize queueJob (#10407)

related: https://github.com/vuejs/core-vapor/pull/138
This commit is contained in:
Evan You 2024-02-26 10:22:12 +08:00 committed by GitHub
parent 58d827cb71
commit 55660b0cfc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 100 additions and 74 deletions

View File

@ -126,10 +126,6 @@ export class ReactiveEffect<T = any>
* @internal
*/
nextEffect?: ReactiveEffect = undefined
/**
* @internal
*/
allowRecurse?: boolean
scheduler?: EffectScheduler = undefined
onStop?: () => void
@ -144,7 +140,10 @@ export class ReactiveEffect<T = any>
* @internal
*/
notify() {
if (this.flags & EffectFlags.RUNNING && !this.allowRecurse) {
if (
this.flags & EffectFlags.RUNNING &&
!(this.flags & EffectFlags.ALLOW_RECURSE)
) {
return
}
if (this.flags & EffectFlags.NO_BATCH) {

View File

@ -1,4 +1,6 @@
import {
type SchedulerJob,
SchedulerJobFlags,
flushPostFlushCbs,
flushPreFlushCbs,
invalidateJob,
@ -119,12 +121,12 @@ describe('scheduler', () => {
const job1 = () => {
calls.push('job1')
}
const cb1 = () => {
const cb1: SchedulerJob = () => {
// queueJob in postFlushCb
calls.push('cb1')
queueJob(job1)
}
cb1.pre = true
cb1.flags! |= SchedulerJobFlags.PRE
queueJob(cb1)
await nextTick()
@ -138,25 +140,25 @@ describe('scheduler', () => {
}
job1.id = 1
const cb1 = () => {
const cb1: SchedulerJob = () => {
calls.push('cb1')
queueJob(job1)
// cb2 should execute before the job
queueJob(cb2)
queueJob(cb3)
}
cb1.pre = true
cb1.flags! |= SchedulerJobFlags.PRE
const cb2 = () => {
const cb2: SchedulerJob = () => {
calls.push('cb2')
}
cb2.pre = true
cb2.flags! |= SchedulerJobFlags.PRE
cb2.id = 1
const cb3 = () => {
const cb3: SchedulerJob = () => {
calls.push('cb3')
}
cb3.pre = true
cb3.flags! |= SchedulerJobFlags.PRE
cb3.id = 1
queueJob(cb1)
@ -166,37 +168,37 @@ describe('scheduler', () => {
it('should insert jobs after pre jobs with the same id', async () => {
const calls: string[] = []
const job1 = () => {
const job1: SchedulerJob = () => {
calls.push('job1')
}
job1.id = 1
job1.pre = true
const job2 = () => {
job1.flags! |= SchedulerJobFlags.PRE
const job2: SchedulerJob = () => {
calls.push('job2')
queueJob(job5)
queueJob(job6)
}
job2.id = 2
job2.pre = true
const job3 = () => {
job2.flags! |= SchedulerJobFlags.PRE
const job3: SchedulerJob = () => {
calls.push('job3')
}
job3.id = 2
job3.pre = true
const job4 = () => {
job3.flags! |= SchedulerJobFlags.PRE
const job4: SchedulerJob = () => {
calls.push('job4')
}
job4.id = 3
job4.pre = true
const job5 = () => {
job4.flags! |= SchedulerJobFlags.PRE
const job5: SchedulerJob = () => {
calls.push('job5')
}
job5.id = 2
const job6 = () => {
const job6: SchedulerJob = () => {
calls.push('job6')
}
job6.id = 2
job6.pre = true
job6.flags! |= SchedulerJobFlags.PRE
// We need several jobs to test this properly, otherwise
// findInsertionIndex can yield the correct index by chance
@ -221,16 +223,16 @@ describe('scheduler', () => {
flushPreFlushCbs()
calls.push('job1')
}
const cb1 = () => {
const cb1: SchedulerJob = () => {
calls.push('cb1')
// a cb triggers its parent job, which should be skipped
queueJob(job1)
}
cb1.pre = true
const cb2 = () => {
cb1.flags! |= SchedulerJobFlags.PRE
const cb2: SchedulerJob = () => {
calls.push('cb2')
}
cb2.pre = true
cb2.flags! |= SchedulerJobFlags.PRE
queueJob(job1)
await nextTick()
@ -240,8 +242,8 @@ describe('scheduler', () => {
// #3806
it('queue preFlushCb inside postFlushCb', async () => {
const spy = vi.fn()
const cb = () => spy()
cb.pre = true
const cb: SchedulerJob = () => spy()
cb.flags! |= SchedulerJobFlags.PRE
queuePostFlushCb(() => {
queueJob(cb)
})
@ -521,25 +523,25 @@ describe('scheduler', () => {
test('should allow explicitly marked jobs to trigger itself', async () => {
// normal job
let count = 0
const job = () => {
const job: SchedulerJob = () => {
if (count < 3) {
count++
queueJob(job)
}
}
job.allowRecurse = true
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
queueJob(job)
await nextTick()
expect(count).toBe(3)
// post cb
const cb = () => {
const cb: SchedulerJob = () => {
if (count < 5) {
count++
queuePostFlushCb(cb)
}
}
cb.allowRecurse = true
cb.flags! |= SchedulerJobFlags.ALLOW_RECURSE
queuePostFlushCb(cb)
await nextTick()
expect(count).toBe(5)
@ -572,7 +574,7 @@ describe('scheduler', () => {
// simulate parent component that toggles child
const job1 = () => {
// @ts-expect-error
job2.active = false
job2.flags! |= SchedulerJobFlags.DISPOSED
}
// simulate child that's triggered by the same reactive change that
// triggers its toggle
@ -589,11 +591,11 @@ describe('scheduler', () => {
it('flushPreFlushCbs inside a pre job', async () => {
const spy = vi.fn()
const job = () => {
const job: SchedulerJob = () => {
spy()
flushPreFlushCbs()
}
job.pre = true
job.flags! |= SchedulerJobFlags.PRE
queueJob(job)
await nextTick()
expect(spy).toHaveBeenCalledTimes(1)

View File

@ -11,7 +11,7 @@ import {
isRef,
isShallow,
} from '@vue/reactivity'
import { type SchedulerJob, queueJob } from './scheduler'
import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
import {
EMPTY_OBJ,
NOOP,
@ -382,7 +382,7 @@ function doWatch(
// important: mark the job as a watcher callback so that scheduler knows
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb
if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
const effect = new ReactiveEffect(getter)
@ -394,7 +394,7 @@ function doWatch(
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
job.pre = true
job.flags! |= SchedulerJobFlags.PRE
if (instance) job.id = instance.uid
scheduler = () => queueJob(job)
}

View File

@ -19,6 +19,7 @@ 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[]
@ -231,7 +232,7 @@ const BaseTransitionImpl: ComponentOptions = {
state.isLeaving = false
// #6835
// it also needs to be updated when active is undefined
if (instance.job.active !== false) {
if (!(instance.job.flags! & SchedulerJobFlags.DISPOSED)) {
instance.update()
}
}

View File

@ -39,13 +39,19 @@ import {
} from '@vue/shared'
import {
type SchedulerJob,
SchedulerJobFlags,
flushPostFlushCbs,
flushPreFlushCbs,
invalidateJob,
queueJob,
queuePostFlushCb,
} from './scheduler'
import { ReactiveEffect, pauseTracking, resetTracking } from '@vue/reactivity'
import {
EffectFlags,
ReactiveEffect,
pauseTracking,
resetTracking,
} from '@vue/reactivity'
import { updateProps } from './componentProps'
import { updateSlots } from './componentSlots'
import { popWarningContext, pushWarningContext, warn } from './warning'
@ -2281,7 +2287,7 @@ function baseCreateRenderer(
// setup has resolved.
if (job) {
// so that scheduler will no longer invoke it
job.active = false
job.flags! |= SchedulerJobFlags.DISPOSED
unmount(subTree, instance, parentSuspense, doRemove)
}
// unmounted hook
@ -2419,7 +2425,13 @@ function toggleRecurse(
{ effect, job }: ComponentInternalInstance,
allowed: boolean,
) {
effect.allowRecurse = job.allowRecurse = allowed
if (allowed) {
effect.flags |= EffectFlags.ALLOW_RECURSE
job.flags! |= SchedulerJobFlags.ALLOW_RECURSE
} else {
effect.flags &= ~EffectFlags.ALLOW_RECURSE
job.flags! &= ~SchedulerJobFlags.ALLOW_RECURSE
}
}
export function needTransition(

View File

@ -2,10 +2,9 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling'
import { type Awaited, NOOP, isArray } from '@vue/shared'
import { type ComponentInternalInstance, getComponentName } from './component'
export interface SchedulerJob extends Function {
id?: number
pre?: boolean
active?: boolean
export enum SchedulerJobFlags {
QUEUED = 1 << 0,
PRE = 1 << 1,
/**
* Indicates whether the effect is allowed to recursively trigger itself
* when managed by the scheduler.
@ -21,7 +20,17 @@ export interface SchedulerJob extends Function {
* responsibility to perform recursive state mutation that eventually
* stabilizes (#1727).
*/
allowRecurse?: boolean
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
/**
* Attached by renderer.ts when setting up a component's render effect
* Used to obtain component information when reporting max recursive updates.
@ -69,7 +78,10 @@ function findInsertionIndex(id: number) {
const middle = (start + end) >>> 1
const middleJob = queue[middle]
const middleJobId = getId(middleJob)
if (middleJobId < id || (middleJobId === id && middleJob.pre)) {
if (
middleJobId < id ||
(middleJobId === id && middleJob.flags! & SchedulerJobFlags.PRE)
) {
start = middle + 1
} else {
end = middle
@ -80,24 +92,22 @@ function findInsertionIndex(id: number) {
}
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
// so it cannot recursively trigger itself again.
// if the job is a watch() callback, the search will start with a +1 index to
// allow it recursively trigger itself - it is the user's responsibility to
// ensure it doesn't end up in an infinite loop.
if (
!queue.length ||
!queue.includes(
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,
)
) {
if (!(job.flags! & SchedulerJobFlags.QUEUED)) {
if (job.id == null) {
queue.push(job)
} else if (
// fast path when the job id is larger than the tail
!(job.flags! & SchedulerJobFlags.PRE) &&
job.id >= (queue[queue.length - 1]?.id || 0)
) {
queue.push(job)
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
if (!(job.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
job.flags! |= SchedulerJobFlags.QUEUED
}
queueFlush()
}
}
@ -118,14 +128,11 @@ export function invalidateJob(job: SchedulerJob) {
export function queuePostFlushCb(cb: SchedulerJobs) {
if (!isArray(cb)) {
if (
!activePostFlushCbs ||
!activePostFlushCbs.includes(
cb,
cb.allowRecurse ? postFlushIndex + 1 : postFlushIndex,
)
) {
if (!(cb.flags! & SchedulerJobFlags.QUEUED)) {
pendingPostFlushCbs.push(cb)
if (!(cb.flags! & SchedulerJobFlags.ALLOW_RECURSE)) {
cb.flags! |= SchedulerJobFlags.QUEUED
}
}
} else {
// if cb is an array, it is a component lifecycle hook which can only be
@ -147,7 +154,7 @@ export function flushPreFlushCbs(
}
for (; i < queue.length; i++) {
const cb = queue[i]
if (cb && cb.pre) {
if (cb && cb.flags! & SchedulerJobFlags.PRE) {
if (instance && cb.id !== instance.uid) {
continue
}
@ -157,6 +164,7 @@ export function flushPreFlushCbs(
queue.splice(i, 1)
i--
cb()
cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
@ -191,6 +199,7 @@ export function flushPostFlushCbs(seen?: CountMap) {
continue
}
activePostFlushCbs[postFlushIndex]()
activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED
}
activePostFlushCbs = null
postFlushIndex = 0
@ -203,8 +212,10 @@ const getId = (job: SchedulerJob): number =>
const comparator = (a: SchedulerJob, b: SchedulerJob): number => {
const diff = getId(a) - getId(b)
if (diff === 0) {
if (a.pre && !b.pre) return -1
if (b.pre && !a.pre) return 1
const isAPre = a.flags! & SchedulerJobFlags.PRE
const isBPre = b.flags! & SchedulerJobFlags.PRE
if (isAPre && !isBPre) return -1
if (isBPre && !isAPre) return 1
}
return diff
}
@ -237,11 +248,12 @@ function flushJobs(seen?: CountMap) {
try {
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
if (job && !(job.flags! & SchedulerJobFlags.DISPOSED)) {
if (__DEV__ && check(job)) {
continue
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
job.flags! &= ~SchedulerJobFlags.QUEUED
}
}
} finally {