diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 483b8a0b431..b797fc61e4f 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -65,12 +65,16 @@ export interface BaseWatchOptions extends DebuggerOptions { deep?: boolean once?: boolean scheduler?: Scheduler - handlerError?: HandleError - handlerWarn?: HandleWarn + handleError?: HandleError + handleWarn?: HandleWarn } export type WatchStopHandle = () => void +export interface WatchInstance extends WatchStopHandle { + effect?: ReactiveEffect +} + // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} @@ -112,20 +116,12 @@ export function baseWatch( onTrack, onTrigger, scheduler = DEFAULT_SCHEDULER, - handlerError = DEFAULT_HANDLE_ERROR, - handlerWarn = warn + handleError: handleError = DEFAULT_HANDLE_ERROR, + handleWarn: handleWarn = warn }: BaseWatchOptions = EMPTY_OBJ -): WatchStopHandle { - if (cb && once) { - const _cb = cb - cb = (...args) => { - _cb(...args) - unwatch() - } - } - +): WatchInstance { const warnInvalidSource = (s: unknown) => { - handlerWarn( + handleWarn( `Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + @@ -155,7 +151,7 @@ export function baseWatch( } else if (isFunction(s)) { return callWithErrorHandling( s, - handlerError, + handleError, BaseWatchErrorCodes.WATCH_GETTER ) } else { @@ -168,16 +164,12 @@ export function baseWatch( getter = () => callWithErrorHandling( source, - handlerError, + handleError, BaseWatchErrorCodes.WATCH_GETTER ) } else { // no cb -> simple effect getter = () => { - // TODO: move to scheduler - // if (instance && instance.isUnmounted) { - // return - // } if (cleanup) { cleanup() } @@ -186,7 +178,7 @@ export function baseWatch( try { return callWithAsyncErrorHandling( source, - handlerError, + handleError, BaseWatchErrorCodes.WATCH_CALLBACK, [onEffectCleanup] ) @@ -205,29 +197,26 @@ export function baseWatch( getter = () => traverse(baseGetter()) } - // TODO: support SSR - // in SSR there is no need to setup an actual effect, and it should be noop - // unless it's eager or sync flush - // let ssrCleanup: (() => void)[] | undefined - // if (__SSR__ && isInSSRComponentSetup) { - // // we will also not call the invalidate callback (+ runner is not set up) - // onCleanup = NOOP - // if (!cb) { - // getter() - // } else if (immediate) { - // callWithAsyncErrorHandling(cb, handlerError, BaseWatchErrorCodes.WATCH_CALLBACK, [ - // getter(), - // isMultiSource ? [] : undefined, - // onCleanup - // ]) - // } - // if (flush === 'sync') { - // const ctx = useSSRContext()! - // ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) - // } else { - // return NOOP - // } - // } + if (once) { + if (!cb) { + getter() + return NOOP + } + if (immediate) { + callWithAsyncErrorHandling( + cb, + handleError, + BaseWatchErrorCodes.WATCH_CALLBACK, + [getter(), isMultiSource ? [] : undefined, onEffectCleanup] + ) + return NOOP + } + const _cb = cb + cb = (...args) => { + _cb(...args) + unwatch() + } + } let oldValue: any = isMultiSource ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) @@ -255,7 +244,7 @@ export function baseWatch( try { callWithAsyncErrorHandling( cb, - handlerError, + handleError, BaseWatchErrorCodes.WATCH_CALLBACK, [ newValue, @@ -295,18 +284,21 @@ export function baseWatch( const cleanup = (effect.onStop = () => { const cleanups = cleanupMap.get(effect) if (cleanups) { - cleanups.forEach(cleanup => cleanup()) + cleanups.forEach(cleanup => + callWithErrorHandling( + cleanup, + handleError, + BaseWatchErrorCodes.WATCH_CLEANUP + ) + ) cleanupMap.delete(effect) } }) - const unwatch = () => { + const unwatch: WatchInstance = () => { effect.stop() - // TODO: move to doWatch - // if (instance && instance.scope) { - // remove(instance.scope.effects!, effect) - // } } + unwatch.effect = effect if (__DEV__) { effect.onTrack = onTrack diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 3735548e7ff..035477ef687 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -69,4 +69,8 @@ export { onScopeDispose } from './effectScope' export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' -export { baseWatch, BaseWatchErrorCodes } from './baseWatch' +export { + baseWatch, + BaseWatchErrorCodes, + type BaseWatchOptions +} from './baseWatch' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 5d343e61a60..52fcb359217 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -1,24 +1,25 @@ import { isRef, - isShallow, - Ref, - ComputedRef, - ReactiveEffect, - isReactive, + type Ref, + type ComputedRef, ReactiveFlags, - EffectScheduler, - DebuggerOptions, + type DebuggerOptions, getCurrentScope, - BaseWatchErrorCodes + BaseWatchErrorCodes, + baseWatch, + type BaseWatchOptions } from '@vue/reactivity' -import { SchedulerJob, queueJob } from './scheduler' +import { + type SchedulerJob, + usePreScheduler, + useSyncScheduler +} from './scheduler' import { EMPTY_OBJ, isObject, isArray, isFunction, isString, - hasChanged, NOOP, remove, isMap, @@ -28,18 +29,15 @@ import { } from '@vue/shared' import { currentInstance, - ComponentInternalInstance, + type ComponentInternalInstance, isInSSRComponentSetup, setCurrentInstance, unsetCurrentInstance } from './component' -import { - callWithErrorHandling, - callWithAsyncErrorHandling -} from './errorHandling' -import { queuePostRenderEffect } from './renderer' +import { handleError as handleErrorWithInstance } from './errorHandling' +import { usePostRenderScheduler } from './renderer' import { warn } from './warning' -import { ObjectWatchOptionItem } from './componentOptions' +import { type ObjectWatchOptionItem } from './componentOptions' import { useSSRContext } from '@vue/runtime-core' export type WatchEffect = (onCleanup: OnCleanup) => void @@ -108,9 +106,6 @@ export function watchSyncEffect( ) } -// initial value for watchers to trigger on undefined initial values -const INITIAL_WATCHER_VALUE = {} - type MultiWatchSources = (WatchSource | object)[] // overload: array of multiple sources + cb @@ -168,19 +163,25 @@ export function watch = false>( return doWatch(source as any, cb, options) } +function getSchedulerByFlushMode( + flush: WatchOptionsBase['flush'] +): SchedulerJob { + if (flush === 'post') { + return usePostRenderScheduler + } + if (flush === 'sync') { + return useSyncScheduler + } + // default: 'pre' + return usePreScheduler +} + function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, - { immediate, deep, flush, once, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ + options: WatchOptions = EMPTY_OBJ ): WatchStopHandle { - if (cb && once) { - const _cb = cb - cb = (...args) => { - _cb(...args) - unwatch() - } - } - + const { immediate, deep, flush, once } = options if (__DEV__ && !cb) { if (immediate !== undefined) { warn( @@ -202,203 +203,38 @@ function doWatch( } } - const warnInvalidSource = (s: unknown) => { - warn( - `Invalid watch source: `, - s, - `A watch source can only be a getter/effect function, a ref, ` + - `a reactive object, or an array of these types.` - ) - } - - const instance = - getCurrentScope() === currentInstance?.scope ? currentInstance : null - // const instance = currentInstance - let getter: () => any - let forceTrigger = false - let isMultiSource = false - - if (isRef(source)) { - getter = () => source.value - forceTrigger = isShallow(source) - } else if (isReactive(source)) { - getter = () => source - deep = true - } else if (isArray(source)) { - isMultiSource = true - forceTrigger = source.some(s => isReactive(s) || isShallow(s)) - getter = () => - source.map(s => { - if (isRef(s)) { - return s.value - } else if (isReactive(s)) { - return traverse(s) - } else if (isFunction(s)) { - return callWithErrorHandling( - s, - instance, - BaseWatchErrorCodes.WATCH_GETTER - ) - } else { - __DEV__ && warnInvalidSource(s) - } - }) - } else if (isFunction(source)) { - if (cb) { - // getter with cb - getter = () => - callWithErrorHandling( - source, - instance, - BaseWatchErrorCodes.WATCH_GETTER - ) - } else { - // no cb -> simple effect - getter = () => { - if (instance && instance.isUnmounted) { - return - } - if (cleanup) { - cleanup() - } - return callWithAsyncErrorHandling( - source, - instance, - BaseWatchErrorCodes.WATCH_CALLBACK, - [onCleanup] - ) - } - } - } else { - getter = NOOP - __DEV__ && warnInvalidSource(source) - } - - if (cb && deep) { - const baseGetter = getter - getter = () => traverse(baseGetter()) - } - - let cleanup: (() => void) | undefined - let onCleanup: OnCleanup = (fn: () => void) => { - cleanup = effect.onStop = () => { - callWithErrorHandling(fn, instance, BaseWatchErrorCodes.WATCH_CLEANUP) - cleanup = effect.onStop = undefined - } - } + const extendOptions: BaseWatchOptions = { handleWarn: warn } - // in SSR there is no need to setup an actual effect, and it should be noop - // unless it's eager or sync flush let ssrCleanup: (() => void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { - // we will also not call the invalidate callback (+ runner is not set up) - onCleanup = NOOP - if (!cb) { - getter() - } else if (immediate) { - callWithAsyncErrorHandling( - cb, - instance, - BaseWatchErrorCodes.WATCH_CALLBACK, - [getter(), isMultiSource ? [] : undefined, onCleanup] - ) - } if (flush === 'sync') { const ctx = useSSRContext()! ssrCleanup = ctx.__watcherHandles || (ctx.__watcherHandles = []) + } else if (!cb || immediate) { + // immediately watch or watchEffect + extendOptions.once = true } else { + // watch(source, cb) return NOOP } } - let oldValue: any = isMultiSource - ? new Array((source as []).length).fill(INITIAL_WATCHER_VALUE) - : INITIAL_WATCHER_VALUE - const job: SchedulerJob = () => { - if (!effect.active || !effect.dirty) { - return - } - if (cb) { - // watch(source, cb) - const newValue = effect.run() - if ( - deep || - forceTrigger || - (isMultiSource - ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) - : hasChanged(newValue, oldValue)) - ) { - // cleanup before running cb again - if (cleanup) { - cleanup() - } - callWithAsyncErrorHandling( - cb, - instance, - BaseWatchErrorCodes.WATCH_CALLBACK, - [ - newValue, - // pass undefined as the old value when it's changed for the first time - oldValue === INITIAL_WATCHER_VALUE - ? undefined - : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE - ? [] - : oldValue, - onCleanup - ] - ) - oldValue = newValue - } - } else { - // watchEffect - effect.run() - } - } + const instance = + getCurrentScope() === currentInstance?.scope ? currentInstance : null - // important: mark the job as a watcher callback so that scheduler knows - // it is allowed to self-trigger (#1727) - job.allowRecurse = !!cb + extendOptions.handleError = (err: unknown, type: BaseWatchErrorCodes) => + handleErrorWithInstance(err, instance, type) - let scheduler: EffectScheduler - if (flush === 'sync') { - scheduler = job as any // the scheduler function gets called directly - } else if (flush === 'post') { - scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) - } else { - // default: 'pre' - job.pre = true - if (instance) job.id = instance.uid - scheduler = () => queueJob(job) - } + const scheduler = getSchedulerByFlushMode(flush)({ instance }) + extendOptions.scheduler = scheduler - const effect = new ReactiveEffect(getter, NOOP, scheduler) + let baseUnwatch = baseWatch(source, cb, extend({}, options, extendOptions)) const unwatch = () => { - effect.stop() + baseUnwatch() if (instance && instance.scope) { - remove(instance.scope.effects!, effect) - } - } - - if (__DEV__) { - effect.onTrack = onTrack - effect.onTrigger = onTrigger - } - - // initial run - if (cb) { - if (immediate) { - job() - } else { - oldValue = effect.run() + remove(instance.scope.effects!, baseUnwatch.effect) } - } else if (flush === 'post') { - queuePostRenderEffect( - effect.run.bind(effect), - instance && instance.suspense - ) - } else { - effect.run() } if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 07c273a0922..3ab02141628 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -948,7 +948,6 @@ export function createWatcher( const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null - console.log('createWatcher') const newValue = getter() if ( isArray(newValue) && diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 62b215eea88..4c646faa5ca 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -43,7 +43,8 @@ import { flushPostFlushCbs, invalidateJob, flushPreFlushCbs, - SchedulerJob + type SchedulerJob, + type Scheduler } from './scheduler' import { pauseTracking, resetTracking, ReactiveEffect } from '@vue/reactivity' import { updateProps } from './componentProps' @@ -281,6 +282,22 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__ : queueEffectWithSuspense : queuePostFlushCb +export const usePostRenderScheduler: Scheduler = + ({ instance }) => + ({ isInit, effect, job }) => { + if (instance && instance.isUnmounted) { + return + } + if (isInit) { + queuePostRenderEffect( + effect.run.bind(effect), + instance && instance.suspense + ) + } else { + queuePostRenderEffect(job, instance && instance.suspense) + } + } + /** * The createRenderer function accepts two generic arguments: * HostNode and HostElement, corresponding to Node and Element types in the diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 0b317581063..dc4bed2c4eb 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,6 +1,7 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' import { Awaited, isArray, NOOP } from '@vue/shared' import { ComponentInternalInstance, getComponentName } from './component' +import { ReactiveEffect } from '@vue/reactivity' export interface SchedulerJob extends Function { id?: number @@ -287,3 +288,39 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) { } } } + +export type Scheduler = (options: { + instance: ComponentInternalInstance | null +}) => (context: { + effect: ReactiveEffect + job: SchedulerJob + isInit: boolean +}) => void + +export const useSyncScheduler: Scheduler = + ({ instance }) => + ({ isInit, effect, job }) => { + if (instance && instance.isUnmounted) { + return + } + if (isInit) { + effect.run() + } else { + job() + } + } + +export const usePreScheduler: Scheduler = + ({ instance }) => + ({ isInit, effect, job }) => { + if (instance && instance.isUnmounted) { + return + } + if (isInit) { + effect.run() + } else { + job.pre = true + if (instance) job.id = instance.uid + queueJob(job) + } + }