mirror of https://github.com/vuejs/core.git
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:
parent
58d827cb71
commit
55660b0cfc
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Reference in New Issue