diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index 9b344912900..d510dd56b8c 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -1,7 +1,6 @@ import { EffectScope, type Ref, - type SchedulerJob, WatchErrorCodes, type WatchScheduler, onWatcherCleanup, @@ -9,7 +8,7 @@ import { watch, } from '../src' -const queue: SchedulerJob[] = [] +const queue: (() => void)[] = [] // a simple scheduler for testing purposes let isFlushPending = false @@ -19,7 +18,7 @@ const nextTick = (fn?: () => any) => const scheduler: WatchScheduler = (job, isFirstRun) => { if (isFirstRun) { - job(true) + job() } else { queue.push(job) flushJobs() diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 335e97c8677..a95e2455b94 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -89,4 +89,3 @@ export { type WatchOptions, type WatchScheduler, } from './watch' -export { type SchedulerJob, SchedulerJobFlags } from './scheduler' diff --git a/packages/reactivity/src/scheduler.ts b/packages/reactivity/src/scheduler.ts deleted file mode 100644 index 709b12cbf52..00000000000 --- a/packages/reactivity/src/scheduler.ts +++ /dev/null @@ -1,30 +0,0 @@ -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 -} diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 3a2e67d17c8..8bddc298f86 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -23,7 +23,6 @@ import { } from './effect' import { isReactive, isShallow } from './reactive' import { type Ref, isRef } from './ref' -import { type SchedulerJob, SchedulerJobFlags } from './scheduler' // These errors were transferred from `packages/runtime-core/src/errorHandling.ts` // to @vue/reactivity to allow co-location with the moved base watch logic, hence @@ -48,6 +47,7 @@ export interface WatchOptions extends DebuggerOptions { deep?: boolean | number once?: boolean scheduler?: WatchScheduler + augmentJob?: (job: (...args: any[]) => void) => void onError?: HandleError onWarn?: HandleWarn } @@ -55,7 +55,7 @@ export interface WatchOptions extends DebuggerOptions { // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} -export type WatchScheduler = (job: SchedulerJob, isFirstRun: boolean) => void +export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void export type HandleError = (err: unknown, type: WatchErrorCodes) => void export type HandleWarn = (msg: string, ...args: any[]) => void @@ -105,6 +105,7 @@ export function watch( deep, once, scheduler, + augmentJob, onWarn = __DEV__ ? warn : NOOP, onError = DEFAULT_HANDLE_ERROR, onTrack, @@ -217,7 +218,8 @@ export function watch( let oldValue: any = isMultiSource ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) : INITIAL_WATCHER_VALUE - const job: SchedulerJob = (immediateFirstRun?: boolean) => { + + const job = (immediateFirstRun?: boolean) => { if ( !(effect.flags & EffectFlags.ACTIVE) || (!effect.dirty && !immediateFirstRun) @@ -267,9 +269,9 @@ export function watch( } } - // important: mark the job as a watcher callback so that scheduler knows - // it is allowed to self-trigger (#1727) - if (cb) job.flags! |= SchedulerJobFlags.ALLOW_RECURSE + if (augmentJob) { + augmentJob(job) + } effect = new ReactiveEffect(getter) if (scheduler) { diff --git a/packages/runtime-core/__tests__/scheduler.spec.ts b/packages/runtime-core/__tests__/scheduler.spec.ts index ef752061ec2..5c5b04673ab 100644 --- a/packages/runtime-core/__tests__/scheduler.spec.ts +++ b/packages/runtime-core/__tests__/scheduler.spec.ts @@ -1,6 +1,6 @@ -import { SchedulerJobFlags } from '@vue/reactivity' import { type SchedulerJob, + SchedulerJobFlags, flushPostFlushCbs, flushPreFlushCbs, nextTick, diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 3cad946e981..4b807e741a7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -4,12 +4,11 @@ import { type DebuggerOptions, type ReactiveMarker, type Ref, - SchedulerJobFlags, type WatchErrorCodes, watch as baseWatch, getCurrentScope, } from '@vue/reactivity' -import { type SchedulerJob, queueJob } from './scheduler' +import { type SchedulerJob, SchedulerJobFlags, queueJob } from './scheduler' import { EMPTY_OBJ, NOOP, @@ -211,26 +210,38 @@ function doWatch( handleErrorWithInstance(err, instance, type) // scheduler + let isPre = false if (flush === 'post') { baseWatchOptions.scheduler = job => { queuePostRenderEffect(job, instance && instance.suspense) } } else if (flush !== 'sync') { // default: 'pre' + isPre = true baseWatchOptions.scheduler = (job, isFirstRun) => { if (isFirstRun) { job() } else { - job.flags! |= SchedulerJobFlags.PRE - if (instance) { - job.id = instance.uid - ;(job as SchedulerJob).i = instance - } queueJob(job) } } } + baseWatchOptions.augmentJob = (job: SchedulerJob) => { + // important: mark the job as a watcher callback so that scheduler knows + // it is allowed to self-trigger (#1727) + if (cb) { + job.flags! |= SchedulerJobFlags.ALLOW_RECURSE + } + if (isPre) { + job.flags! |= SchedulerJobFlags.PRE + if (instance) { + job.id = instance.uid + ;(job as SchedulerJob).i = instance + } + } + } + const effect = baseWatch(source, cb, baseWatchOptions) const scope = getCurrentScope() const watchHandle: WatchHandle = () => { diff --git a/packages/runtime-core/src/components/BaseTransition.ts b/packages/runtime-core/src/components/BaseTransition.ts index f40f3365ac0..a31f28b2388 100644 --- a/packages/runtime-core/src/components/BaseTransition.ts +++ b/packages/runtime-core/src/components/BaseTransition.ts @@ -14,12 +14,13 @@ import { } from '../vnode' import { warn } from '../warning' import { isKeepAlive } from './KeepAlive' -import { SchedulerJobFlags, toRaw } from '@vue/reactivity' +import { toRaw } from '@vue/reactivity' import { ErrorCodes, callWithAsyncErrorHandling } from '../errorHandling' import { PatchFlags, ShapeFlags, isArray, isFunction } from '@vue/shared' import { onBeforeUnmount, onMounted } from '../apiLifecycle' import { isTeleport } from './Teleport' import type { RendererElement } from '../renderer' +import { SchedulerJobFlags } from '../scheduler' type Hook void> = T | T[] diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index fc1fbc8d429..11736e9dff2 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -41,6 +41,7 @@ import { } from '@vue/shared' import { type SchedulerJob, + SchedulerJobFlags, type SchedulerJobs, flushPostFlushCbs, flushPreFlushCbs, @@ -50,7 +51,6 @@ import { import { EffectFlags, ReactiveEffect, - SchedulerJobFlags, pauseTracking, resetTracking, } from '@vue/reactivity' diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index cbfcd30838f..aa12b6896a7 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,12 +1,36 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' import { type Awaited, NOOP, isArray } from '@vue/shared' import { type ComponentInternalInstance, getComponentName } from './component' -import { - type SchedulerJob as BaseSchedulerJob, - SchedulerJobFlags, -} from '@vue/reactivity' -export interface SchedulerJob extends BaseSchedulerJob { +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 /** * Attached by renderer.ts when setting up a component's render effect * Used to obtain component information when reporting max recursive updates.