* @internal
*/
nextEffect?: ReactiveEffect = undefined
- /**
- * @internal
- */
- allowRecurse?: boolean
scheduler?: EffectScheduler = undefined
onStop?: () => void
* @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) {
import {
+ type SchedulerJob,
+ SchedulerJobFlags,
flushPostFlushCbs,
flushPreFlushCbs,
invalidateJob,
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()
}
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)
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
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()
// #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)
})
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)
// 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
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)
isRef,
isShallow,
} from '@vue/reactivity'
-import { type SchedulerJob, queueJob } from './scheduler'
+import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler'
import {
EMPTY_OBJ,
NOOP,
// 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)
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)
}
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[]
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()
}
}
} 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'
// 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
{ 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(
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.
* 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.
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
}
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()
}
}
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
}
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
}
queue.splice(i, 1)
i--
cb()
+ cb.flags! &= ~SchedulerJobFlags.QUEUED
}
}
}
continue
}
activePostFlushCbs[postFlushIndex]()
+ activePostFlushCbs[postFlushIndex].flags! &= ~SchedulerJobFlags.QUEUED
}
activePostFlushCbs = null
postFlushIndex = 0
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
}
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 {