From f1fe01e7f0083c2eda65edde915143768fa5f839 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Mon, 25 Dec 2023 20:50:35 +0800 Subject: [PATCH 01/18] refactor: externalized COMPAT case --- packages/runtime-core/src/apiWatch.ts | 22 +-------- packages/runtime-core/src/componentOptions.ts | 46 +++++++++++++++---- 2 files changed, 39 insertions(+), 29 deletions(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 5d5b8713f..9b93943cc 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -39,8 +39,6 @@ import { } from './errorHandling' import { queuePostRenderEffect } from './renderer' import { warn } from './warning' -import { DeprecationTypes } from './compat/compatConfig' -import { checkCompatEnabled, isCompatEnabled } from './compat/compatConfig' import { ObjectWatchOptionItem } from './componentOptions' import { useSSRContext } from '@vue/runtime-core' @@ -268,21 +266,6 @@ function doWatch( __DEV__ && warnInvalidSource(source) } - // 2.x array mutation watch compat - if (__COMPAT__ && cb && !deep) { - const baseGetter = getter - getter = () => { - const val = baseGetter() - if ( - isArray(val) && - checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) - ) { - traverse(val) - } - return val - } - } - if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) @@ -334,10 +317,7 @@ function doWatch( forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, oldValue[i])) - : hasChanged(newValue, oldValue)) || - (__COMPAT__ && - isArray(newValue) && - isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) + : hasChanged(newValue, oldValue)) ) { // cleanup before running cb again if (cleanup) { diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 9633cbfe9..07c273a09 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -5,7 +5,8 @@ import { ComponentInternalOptions, Component, ConcreteComponent, - InternalRenderFunction + InternalRenderFunction, + currentInstance } from './component' import { isFunction, @@ -18,13 +19,14 @@ import { LooseRequired, Prettify } from '@vue/shared' -import { isRef, Ref } from '@vue/reactivity' +import { getCurrentScope, isRef, Ref } from '@vue/reactivity' import { computed } from './apiComputed' import { watch, WatchOptions, WatchCallback, - createPathGetter + createPathGetter, + traverse } from './apiWatch' import { provide, inject } from './apiInject' import { @@ -67,7 +69,7 @@ import { warn } from './warning' import { VNodeChild } from './vnode' import { callWithAsyncErrorHandling } from './errorHandling' import { deepMergeData } from './compat/data' -import { DeprecationTypes } from './compat/compatConfig' +import { DeprecationTypes, checkCompatEnabled } from './compat/compatConfig' import { CompatConfig, isCompatEnabled, @@ -937,18 +939,46 @@ export function createWatcher( publicThis: ComponentPublicInstance, key: string ) { - const getter = key.includes('.') + let getter = key.includes('.') ? createPathGetter(publicThis, key) : () => (publicThis as any)[key] + + const options: WatchOptions = {} + if (__COMPAT__) { + const instance = + getCurrentScope() === currentInstance?.scope ? currentInstance : null + + console.log('createWatcher') + const newValue = getter() + if ( + isArray(newValue) && + isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) + ) { + options.deep = true + } + + const baseGetter = getter + getter = () => { + const val = baseGetter() + if ( + isArray(val) && + checkCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance) + ) { + traverse(val) + } + return val + } + } + if (isString(raw)) { const handler = ctx[raw] if (isFunction(handler)) { - watch(getter, handler as WatchCallback) + watch(getter, handler as WatchCallback, options) } else if (__DEV__) { warn(`Invalid watch handler specified by key "${raw}"`, handler) } } else if (isFunction(raw)) { - watch(getter, raw.bind(publicThis)) + watch(getter, raw.bind(publicThis), options) } else if (isObject(raw)) { if (isArray(raw)) { raw.forEach(r => createWatcher(r, ctx, publicThis, key)) @@ -957,7 +987,7 @@ export function createWatcher( ? raw.handler.bind(publicThis) : (ctx[raw.handler] as WatchCallback) if (isFunction(handler)) { - watch(getter, handler, raw) + watch(getter, handler, extend(raw, options)) } else if (__DEV__) { warn(`Invalid watch handler specified by key "${raw.handler}"`, handler) } From d8682e8f75376aca044b999cfc82aa51a0065a60 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Mon, 25 Dec 2023 22:09:38 +0800 Subject: [PATCH 02/18] feat: initial code of baseWatch --- packages/reactivity/src/baseWatch.ts | 397 +++++++++++++++++++++ packages/reactivity/src/index.ts | 1 + packages/runtime-core/src/apiWatch.ts | 56 +-- packages/runtime-core/src/errorHandling.ts | 16 +- 4 files changed, 440 insertions(+), 30 deletions(-) create mode 100644 packages/reactivity/src/baseWatch.ts diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts new file mode 100644 index 000000000..483b8a0b4 --- /dev/null +++ b/packages/reactivity/src/baseWatch.ts @@ -0,0 +1,397 @@ +import { + EMPTY_OBJ, + isObject, + isArray, + isFunction, + hasChanged, + NOOP, + isMap, + isSet, + isPlainObject, + isPromise +} from '@vue/shared' +import { warn } from './warning' +import { ComputedRef } from './computed' +import { ReactiveFlags } from './constants' +import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect' +import { isShallow, isReactive } from './reactive' +import { Ref, isRef } from './ref' + +// contexts where user provided function may be executed, in addition to +// lifecycle hooks. +export enum BaseWatchErrorCodes { + WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER', + WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK', + WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP' +} + +export interface SchedulerJob extends Function { + id?: number + pre?: boolean + active?: boolean + computed?: boolean + /** + * 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). + */ + allowRecurse?: boolean +} + +export type WatchEffect = (onCleanup: OnCleanup) => void + +export type WatchSource = Ref | ComputedRef | (() => T) + +export type WatchCallback = ( + value: V, + oldValue: OV, + onCleanup: OnCleanup +) => any + +type OnCleanup = (cleanupFn: () => void) => void + +export interface BaseWatchOptions extends DebuggerOptions { + immediate?: Immediate + deep?: boolean + once?: boolean + scheduler?: Scheduler + handlerError?: HandleError + handlerWarn?: HandleWarn +} + +export type WatchStopHandle = () => void + +// initial value for watchers to trigger on undefined initial values +const INITIAL_WATCHER_VALUE = {} + +export type Scheduler = (context: { + effect: ReactiveEffect + job: SchedulerJob + isInit: boolean +}) => void + +const DEFAULT_SCHEDULER: Scheduler = ({ job }) => job() + +export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void + +const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => { + throw err +} + +export type HandleWarn = (msg: string, ...args: any[]) => void + +const cleanupMap: WeakMap void)[]> = new WeakMap() +let activeEffect: ReactiveEffect | undefined = undefined + +export function onEffectCleanup(cleanupFn: () => void) { + if (activeEffect) { + const cleanups = + cleanupMap.get(activeEffect) || + cleanupMap.set(activeEffect, []).get(activeEffect)! + cleanups.push(cleanupFn) + } +} + +export function baseWatch( + source: WatchSource | WatchSource[] | WatchEffect | object, + cb: WatchCallback | null, + { + immediate, + deep, + once, + onTrack, + onTrigger, + scheduler = DEFAULT_SCHEDULER, + handlerError = DEFAULT_HANDLE_ERROR, + handlerWarn = warn + }: BaseWatchOptions = EMPTY_OBJ +): WatchStopHandle { + if (cb && once) { + const _cb = cb + cb = (...args) => { + _cb(...args) + unwatch() + } + } + + const warnInvalidSource = (s: unknown) => { + handlerWarn( + `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.` + ) + } + + 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, + handlerError, + BaseWatchErrorCodes.WATCH_GETTER + ) + } else { + __DEV__ && warnInvalidSource(s) + } + }) + } else if (isFunction(source)) { + if (cb) { + // getter with cb + getter = () => + callWithErrorHandling( + source, + handlerError, + BaseWatchErrorCodes.WATCH_GETTER + ) + } else { + // no cb -> simple effect + getter = () => { + // TODO: move to scheduler + // if (instance && instance.isUnmounted) { + // return + // } + if (cleanup) { + cleanup() + } + const currentEffect = activeEffect + activeEffect = effect + try { + return callWithAsyncErrorHandling( + source, + handlerError, + BaseWatchErrorCodes.WATCH_CALLBACK, + [onEffectCleanup] + ) + } finally { + activeEffect = currentEffect + } + } + } + } else { + getter = NOOP + __DEV__ && warnInvalidSource(source) + } + + if (cb && deep) { + const baseGetter = getter + 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 + // } + // } + + 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() + } + const currentEffect = activeEffect + activeEffect = effect + try { + callWithAsyncErrorHandling( + cb, + handlerError, + 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, + onEffectCleanup + ] + ) + oldValue = newValue + } finally { + activeEffect = currentEffect + } + } + } else { + // watchEffect + effect.run() + } + } + + // important: mark the job as a watcher callback so that scheduler knows + // it is allowed to self-trigger (#1727) + job.allowRecurse = !!cb + + let effectScheduler: EffectScheduler = () => + scheduler({ + effect, + job, + isInit: false + }) + + const effect = new ReactiveEffect(getter, NOOP, effectScheduler) + + const cleanup = (effect.onStop = () => { + const cleanups = cleanupMap.get(effect) + if (cleanups) { + cleanups.forEach(cleanup => cleanup()) + cleanupMap.delete(effect) + } + }) + + const unwatch = () => { + effect.stop() + // TODO: move to doWatch + // 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() + } + } else { + scheduler({ + effect, + job, + isInit: true + }) + } + + return unwatch +} + +export function traverse(value: unknown, seen?: Set) { + if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { + return value + } + seen = seen || new Set() + if (seen.has(value)) { + return value + } + seen.add(value) + if (isRef(value)) { + traverse(value.value, seen) + } else if (isArray(value)) { + for (let i = 0; i < value.length; i++) { + traverse(value[i], seen) + } + } else if (isSet(value) || isMap(value)) { + value.forEach((v: any) => { + traverse(v, seen) + }) + } else if (isPlainObject(value)) { + for (const key in value) { + traverse(value[key], seen) + } + } + return value +} + +export function callWithErrorHandling( + fn: Function, + handleError: HandleError, + type: BaseWatchErrorCodes, + args?: unknown[] +) { + let res + try { + res = args ? fn(...args) : fn() + } catch (err) { + handleError(err, type) + } + return res +} + +export function callWithAsyncErrorHandling( + fn: Function | Function[], + handleError: HandleError, + type: BaseWatchErrorCodes, + args?: unknown[] +): any[] { + if (isFunction(fn)) { + const res = callWithErrorHandling(fn, handleError, type, args) + if (res && isPromise(res)) { + res.catch(err => { + handleError(err, type) + }) + } + return res + } + + const values = [] + for (let i = 0; i < fn.length; i++) { + values.push(callWithAsyncErrorHandling(fn[i], handleError, type, args)) + } + return values +} diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 2a9615b14..3735548e7 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -69,3 +69,4 @@ export { onScopeDispose } from './effectScope' export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' +export { baseWatch, BaseWatchErrorCodes } from './baseWatch' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 9b93943cc..5d343e61a 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -8,7 +8,8 @@ import { ReactiveFlags, EffectScheduler, DebuggerOptions, - getCurrentScope + getCurrentScope, + BaseWatchErrorCodes } from '@vue/reactivity' import { SchedulerJob, queueJob } from './scheduler' import { @@ -33,7 +34,6 @@ import { unsetCurrentInstance } from './component' import { - ErrorCodes, callWithErrorHandling, callWithAsyncErrorHandling } from './errorHandling' @@ -234,7 +234,11 @@ function doWatch( } else if (isReactive(s)) { return traverse(s) } else if (isFunction(s)) { - return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) + return callWithErrorHandling( + s, + instance, + BaseWatchErrorCodes.WATCH_GETTER + ) } else { __DEV__ && warnInvalidSource(s) } @@ -243,7 +247,11 @@ function doWatch( if (cb) { // getter with cb getter = () => - callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) + callWithErrorHandling( + source, + instance, + BaseWatchErrorCodes.WATCH_GETTER + ) } else { // no cb -> simple effect getter = () => { @@ -256,7 +264,7 @@ function doWatch( return callWithAsyncErrorHandling( source, instance, - ErrorCodes.WATCH_CALLBACK, + BaseWatchErrorCodes.WATCH_CALLBACK, [onCleanup] ) } @@ -274,7 +282,7 @@ function doWatch( let cleanup: (() => void) | undefined let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { - callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) + callWithErrorHandling(fn, instance, BaseWatchErrorCodes.WATCH_CLEANUP) cleanup = effect.onStop = undefined } } @@ -288,11 +296,12 @@ function doWatch( if (!cb) { getter() } else if (immediate) { - callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ - getter(), - isMultiSource ? [] : undefined, - onCleanup - ]) + callWithAsyncErrorHandling( + cb, + instance, + BaseWatchErrorCodes.WATCH_CALLBACK, + [getter(), isMultiSource ? [] : undefined, onCleanup] + ) } if (flush === 'sync') { const ctx = useSSRContext()! @@ -323,16 +332,21 @@ function doWatch( if (cleanup) { cleanup() } - callWithAsyncErrorHandling(cb, instance, ErrorCodes.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 - ]) + 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 { diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index aff4f5567..4f6a2e74b 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -3,15 +3,13 @@ import { ComponentInternalInstance } from './component' import { warn, pushWarningContext, popWarningContext } from './warning' import { isPromise, isFunction } from '@vue/shared' import { LifecycleHooks } from './enums' +import { BaseWatchErrorCodes } from '@vue/reactivity' // contexts where user provided function may be executed, in addition to // lifecycle hooks. export enum ErrorCodes { SETUP_FUNCTION, RENDER_FUNCTION, - WATCH_GETTER, - WATCH_CALLBACK, - WATCH_CLEANUP, NATIVE_EVENT_HANDLER, COMPONENT_EVENT_HANDLER, VNODE_HOOK, @@ -24,7 +22,9 @@ export enum ErrorCodes { SCHEDULER } -export const ErrorTypeStrings: Record = { +export type ErrorTypes = LifecycleHooks | ErrorCodes | BaseWatchErrorCodes + +export const ErrorTypeStrings: Record = { [LifecycleHooks.SERVER_PREFETCH]: 'serverPrefetch hook', [LifecycleHooks.BEFORE_CREATE]: 'beforeCreate hook', [LifecycleHooks.CREATED]: 'created hook', @@ -41,9 +41,9 @@ export const ErrorTypeStrings: Record = { [LifecycleHooks.RENDER_TRIGGERED]: 'renderTriggered hook', [ErrorCodes.SETUP_FUNCTION]: 'setup function', [ErrorCodes.RENDER_FUNCTION]: 'render function', - [ErrorCodes.WATCH_GETTER]: 'watcher getter', - [ErrorCodes.WATCH_CALLBACK]: 'watcher callback', - [ErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function', + [BaseWatchErrorCodes.WATCH_GETTER]: 'watcher getter', + [BaseWatchErrorCodes.WATCH_CALLBACK]: 'watcher callback', + [BaseWatchErrorCodes.WATCH_CLEANUP]: 'watcher cleanup function', [ErrorCodes.NATIVE_EVENT_HANDLER]: 'native event handler', [ErrorCodes.COMPONENT_EVENT_HANDLER]: 'component event handler', [ErrorCodes.VNODE_HOOK]: 'vnode hook', @@ -58,8 +58,6 @@ export const ErrorTypeStrings: Record = { 'Please open an issue at https://new-issue.vuejs.org/?repo=vuejs/core' } -export type ErrorTypes = LifecycleHooks | ErrorCodes - export function callWithErrorHandling( fn: Function, instance: ComponentInternalInstance | null, From 409b52a6bf87c9fdb89b062bffe2cbce776e7218 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 26 Dec 2023 21:31:27 +0800 Subject: [PATCH 03/18] refactor: the watch API with baseWatch --- packages/reactivity/src/baseWatch.ts | 94 +++---- packages/reactivity/src/index.ts | 6 +- packages/runtime-core/src/apiWatch.ts | 252 +++--------------- packages/runtime-core/src/componentOptions.ts | 1 - packages/runtime-core/src/renderer.ts | 19 +- packages/runtime-core/src/scheduler.ts | 37 +++ 6 files changed, 147 insertions(+), 262 deletions(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 483b8a0b4..b797fc61e 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 3735548e7..035477ef6 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 5d343e61a..52fcb3592 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 07c273a09..3ab021416 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 62b215eea..4c646faa5 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 0b3175810..dc4bed2c4 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) + } + } From db4463cd3273d0b13c3ef11448435a84a2e8bcec Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 26 Dec 2023 21:40:12 +0800 Subject: [PATCH 04/18] fix: export onEffectCleanup --- packages/reactivity/src/baseWatch.ts | 1 + packages/reactivity/src/index.ts | 1 + packages/runtime-core/src/index.ts | 1 + 3 files changed, 3 insertions(+) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index b797fc61e..30e725de7 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -25,6 +25,7 @@ export enum BaseWatchErrorCodes { WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP' } +// TODO move to a scheduler package export interface SchedulerJob extends Function { id?: number pre?: boolean diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 035477ef6..b8db85d48 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -71,6 +71,7 @@ export { export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' export { baseWatch, + onEffectCleanup, BaseWatchErrorCodes, type BaseWatchOptions } from './baseWatch' diff --git a/packages/runtime-core/src/index.ts b/packages/runtime-core/src/index.ts index 06300cbf6..e0129ff6d 100644 --- a/packages/runtime-core/src/index.ts +++ b/packages/runtime-core/src/index.ts @@ -28,6 +28,7 @@ export { // effect effect, stop, + onEffectCleanup, ReactiveEffect, // effect scope effectScope, From 2aef6099a35f61eaf35ac62b62b4255188f93442 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Tue, 26 Dec 2023 22:19:26 +0800 Subject: [PATCH 05/18] fix: some cases for server-renderer --- packages/reactivity/src/baseWatch.ts | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 30e725de7..96273cc14 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -16,6 +16,7 @@ import { ReactiveFlags } from './constants' import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect' import { isShallow, isReactive } from './reactive' import { Ref, isRef } from './ref' +import { getCurrentScope } from './effectScope' // contexts where user provided function may be executed, in addition to // lifecycle hooks. @@ -130,7 +131,9 @@ export function baseWatch( ) } + let effect: ReactiveEffect let getter: () => any + let cleanup: (() => void) | undefined let forceTrigger = false let isMultiSource = false @@ -200,10 +203,14 @@ export function baseWatch( if (once) { if (!cb) { + // onEffectCleanup need use effect as a key + getCurrentScope()?.effects.push((effect = {} as any)) getter() return NOOP } if (immediate) { + // onEffectCleanup need use effect as a key + getCurrentScope()?.effects.push((effect = {} as any)) callWithAsyncErrorHandling( cb, handleError, @@ -280,9 +287,9 @@ export function baseWatch( isInit: false }) - const effect = new ReactiveEffect(getter, NOOP, effectScheduler) + effect = new ReactiveEffect(getter, NOOP, effectScheduler) - const cleanup = (effect.onStop = () => { + cleanup = effect.onStop = () => { const cleanups = cleanupMap.get(effect) if (cleanups) { cleanups.forEach(cleanup => @@ -294,7 +301,7 @@ export function baseWatch( ) cleanupMap.delete(effect) } - }) + } const unwatch: WatchInstance = () => { effect.stop() From d1f001b96c7472ee0825abe2d2bdbc2e4450d607 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 27 Dec 2023 20:10:05 +0800 Subject: [PATCH 06/18] fix: lint --- packages/reactivity/src/baseWatch.ts | 56 ++++++++++---------- packages/runtime-core/src/apiWatch.ts | 74 +++++++++++++-------------- 2 files changed, 67 insertions(+), 63 deletions(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 96273cc14..1255aabb9 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -1,21 +1,25 @@ import { EMPTY_OBJ, - isObject, + NOOP, + hasChanged, isArray, isFunction, - hasChanged, - NOOP, isMap, - isSet, + isObject, isPlainObject, - isPromise + isPromise, + isSet, } from '@vue/shared' import { warn } from './warning' -import { ComputedRef } from './computed' +import type { ComputedRef } from './computed' import { ReactiveFlags } from './constants' -import { DebuggerOptions, ReactiveEffect, EffectScheduler } from './effect' -import { isShallow, isReactive } from './reactive' -import { Ref, isRef } from './ref' +import { + type DebuggerOptions, + type EffectScheduler, + ReactiveEffect, +} from './effect' +import { isReactive, isShallow } from './reactive' +import { type Ref, isRef } from './ref' import { getCurrentScope } from './effectScope' // contexts where user provided function may be executed, in addition to @@ -23,7 +27,7 @@ import { getCurrentScope } from './effectScope' export enum BaseWatchErrorCodes { WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER', WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK', - WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP' + WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP', } // TODO move to a scheduler package @@ -57,7 +61,7 @@ export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: V, oldValue: OV, - onCleanup: OnCleanup + onCleanup: OnCleanup, ) => any type OnCleanup = (cleanupFn: () => void) => void @@ -119,15 +123,15 @@ export function baseWatch( onTrigger, scheduler = DEFAULT_SCHEDULER, handleError: handleError = DEFAULT_HANDLE_ERROR, - handleWarn: handleWarn = warn - }: BaseWatchOptions = EMPTY_OBJ + handleWarn: handleWarn = warn, + }: BaseWatchOptions = EMPTY_OBJ, ): WatchInstance { const warnInvalidSource = (s: unknown) => { handleWarn( `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.` + `a reactive object, or an array of these types.`, ) } @@ -156,7 +160,7 @@ export function baseWatch( return callWithErrorHandling( s, handleError, - BaseWatchErrorCodes.WATCH_GETTER + BaseWatchErrorCodes.WATCH_GETTER, ) } else { __DEV__ && warnInvalidSource(s) @@ -169,7 +173,7 @@ export function baseWatch( callWithErrorHandling( source, handleError, - BaseWatchErrorCodes.WATCH_GETTER + BaseWatchErrorCodes.WATCH_GETTER, ) } else { // no cb -> simple effect @@ -184,7 +188,7 @@ export function baseWatch( source, handleError, BaseWatchErrorCodes.WATCH_CALLBACK, - [onEffectCleanup] + [onEffectCleanup], ) } finally { activeEffect = currentEffect @@ -215,7 +219,7 @@ export function baseWatch( cb, handleError, BaseWatchErrorCodes.WATCH_CALLBACK, - [getter(), isMultiSource ? [] : undefined, onEffectCleanup] + [getter(), isMultiSource ? [] : undefined, onEffectCleanup], ) return NOOP } @@ -262,8 +266,8 @@ export function baseWatch( : isMultiSource && oldValue[0] === INITIAL_WATCHER_VALUE ? [] : oldValue, - onEffectCleanup - ] + onEffectCleanup, + ], ) oldValue = newValue } finally { @@ -284,7 +288,7 @@ export function baseWatch( scheduler({ effect, job, - isInit: false + isInit: false, }) effect = new ReactiveEffect(getter, NOOP, effectScheduler) @@ -296,8 +300,8 @@ export function baseWatch( callWithErrorHandling( cleanup, handleError, - BaseWatchErrorCodes.WATCH_CLEANUP - ) + BaseWatchErrorCodes.WATCH_CLEANUP, + ), ) cleanupMap.delete(effect) } @@ -324,7 +328,7 @@ export function baseWatch( scheduler({ effect, job, - isInit: true + isInit: true, }) } @@ -362,7 +366,7 @@ export function callWithErrorHandling( fn: Function, handleError: HandleError, type: BaseWatchErrorCodes, - args?: unknown[] + args?: unknown[], ) { let res try { @@ -377,7 +381,7 @@ export function callWithAsyncErrorHandling( fn: Function | Function[], handleError: HandleError, type: BaseWatchErrorCodes, - args?: unknown[] + args?: unknown[], ): any[] { if (isFunction(fn)) { const res = callWithErrorHandling(fn, handleError, type, args) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 52fcb3592..22a8d0fb7 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -1,43 +1,43 @@ import { - isRef, - type Ref, + type BaseWatchErrorCodes, + type BaseWatchOptions, type ComputedRef, - ReactiveFlags, type DebuggerOptions, - getCurrentScope, - BaseWatchErrorCodes, + ReactiveFlags, + type Ref, baseWatch, - type BaseWatchOptions + getCurrentScope, + isRef, } from '@vue/reactivity' import { type SchedulerJob, usePreScheduler, - useSyncScheduler + useSyncScheduler, } from './scheduler' import { EMPTY_OBJ, - isObject, + NOOP, + extend, isArray, isFunction, - isString, - NOOP, - remove, isMap, - isSet, + isObject, isPlainObject, - extend + isSet, + isString, + remove, } from '@vue/shared' import { - currentInstance, type ComponentInternalInstance, + currentInstance, isInSSRComponentSetup, setCurrentInstance, - unsetCurrentInstance + unsetCurrentInstance, } from './component' import { handleError as handleErrorWithInstance } from './errorHandling' import { usePostRenderScheduler } from './renderer' import { warn } from './warning' -import { type ObjectWatchOptionItem } from './componentOptions' +import type { ObjectWatchOptionItem } from './componentOptions' import { useSSRContext } from '@vue/runtime-core' export type WatchEffect = (onCleanup: OnCleanup) => void @@ -47,7 +47,7 @@ export type WatchSource = Ref | ComputedRef | (() => T) export type WatchCallback = ( value: V, oldValue: OV, - onCleanup: OnCleanup + onCleanup: OnCleanup, ) => any type MapSources = { @@ -79,30 +79,30 @@ export type WatchStopHandle = () => void // Simple effect. export function watchEffect( effect: WatchEffect, - options?: WatchOptionsBase + options?: WatchOptionsBase, ): WatchStopHandle { return doWatch(effect, null, options) } export function watchPostEffect( effect: WatchEffect, - options?: DebuggerOptions + options?: DebuggerOptions, ) { return doWatch( effect, null, - __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' } + __DEV__ ? extend({}, options as any, { flush: 'post' }) : { flush: 'post' }, ) } export function watchSyncEffect( effect: WatchEffect, - options?: DebuggerOptions + options?: DebuggerOptions, ) { return doWatch( effect, null, - __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' } + __DEV__ ? extend({}, options as any, { flush: 'sync' }) : { flush: 'sync' }, ) } @@ -111,11 +111,11 @@ type MultiWatchSources = (WatchSource | object)[] // overload: array of multiple sources + cb export function watch< T extends MultiWatchSources, - Immediate extends Readonly = false + Immediate extends Readonly = false, >( sources: [...T], cb: WatchCallback, MapSources>, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle // overload: multiple sources w/ `as const` @@ -123,48 +123,48 @@ export function watch< // somehow [...T] breaks when the type is readonly export function watch< T extends Readonly, - Immediate extends Readonly = false + Immediate extends Readonly = false, >( source: T, cb: WatchCallback, MapSources>, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle // overload: single source + cb export function watch = false>( source: WatchSource, cb: WatchCallback, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle // overload: watching reactive object w/ cb export function watch< T extends object, - Immediate extends Readonly = false + Immediate extends Readonly = false, >( source: T, cb: WatchCallback, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle // implementation export function watch = false>( source: T | WatchSource, cb: any, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle { if (__DEV__ && !isFunction(cb)) { warn( `\`watch(fn, options?)\` signature has been moved to a separate API. ` + `Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` + - `supports \`watch(source, cb, options?) signature.` + `supports \`watch(source, cb, options?) signature.`, ) } return doWatch(source as any, cb, options) } function getSchedulerByFlushMode( - flush: WatchOptionsBase['flush'] + flush: WatchOptionsBase['flush'], ): SchedulerJob { if (flush === 'post') { return usePostRenderScheduler @@ -179,26 +179,26 @@ function getSchedulerByFlushMode( function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, - options: WatchOptions = EMPTY_OBJ + options: WatchOptions = EMPTY_OBJ, ): WatchStopHandle { const { immediate, deep, flush, once } = options if (__DEV__ && !cb) { if (immediate !== undefined) { warn( `watch() "immediate" option is only respected when using the ` + - `watch(source, callback, options?) signature.` + `watch(source, callback, options?) signature.`, ) } if (deep !== undefined) { warn( `watch() "deep" option is only respected when using the ` + - `watch(source, callback, options?) signature.` + `watch(source, callback, options?) signature.`, ) } if (once !== undefined) { warn( `watch() "once" option is only respected when using the ` + - `watch(source, callback, options?) signature.` + `watch(source, callback, options?) signature.`, ) } } @@ -246,7 +246,7 @@ export function instanceWatch( this: ComponentInternalInstance, source: string | Function, value: WatchCallback | ObjectWatchOptionItem, - options?: WatchOptions + options?: WatchOptions, ): WatchStopHandle { const publicThis = this.proxy as any const getter = isString(source) From 4d04f5ec11fc43f2bb2c23192bd901ba9c0f88e5 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 27 Dec 2023 20:19:53 +0800 Subject: [PATCH 07/18] fix: treeshaking error --- packages/runtime-core/src/apiWatch.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 22a8d0fb7..051f305d9 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -203,7 +203,9 @@ function doWatch( } } - const extendOptions: BaseWatchOptions = { handleWarn: warn } + const extendOptions: BaseWatchOptions = {} + + if (__DEV__) extendOptions.handleWarn = warn let ssrCleanup: (() => void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { From b57405c0004fdc1f95ab2945d5639eb96eea0bf4 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 27 Dec 2023 20:28:49 +0800 Subject: [PATCH 08/18] fix: treeshaking error --- packages/reactivity/src/baseWatch.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 1255aabb9..fe55a154a 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -123,7 +123,7 @@ export function baseWatch( onTrigger, scheduler = DEFAULT_SCHEDULER, handleError: handleError = DEFAULT_HANDLE_ERROR, - handleWarn: handleWarn = warn, + handleWarn: handleWarn = __DEV__ ? warn : NOOP, }: BaseWatchOptions = EMPTY_OBJ, ): WatchInstance { const warnInvalidSource = (s: unknown) => { From a8dc8e63a992d5ea93750ca20ad1092516d3d4ca Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Wed, 27 Dec 2023 22:43:17 +0800 Subject: [PATCH 09/18] fix: tracked in cleanup --- packages/reactivity/src/baseWatch.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index fe55a154a..f51c00b37 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -17,6 +17,8 @@ import { type DebuggerOptions, type EffectScheduler, ReactiveEffect, + pauseTracking, + resetTracking, } from './effect' import { isReactive, isShallow } from './reactive' import { type Ref, isRef } from './ref' @@ -179,7 +181,12 @@ export function baseWatch( // no cb -> simple effect getter = () => { if (cleanup) { - cleanup() + pauseTracking() + try { + cleanup() + } finally { + resetTracking() + } } const currentEffect = activeEffect activeEffect = effect From 56c87ec1eea8ae55cd5f0ae1cee2566ff638ef08 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 28 Dec 2023 19:44:43 +0800 Subject: [PATCH 10/18] test: baseWatch with onEffectCleanup --- .../reactivity/__tests__/baseWatch.spec.ts | 109 ++++++++++++++++++ packages/reactivity/src/baseWatch.ts | 2 +- 2 files changed, 110 insertions(+), 1 deletion(-) create mode 100644 packages/reactivity/__tests__/baseWatch.spec.ts diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts new file mode 100644 index 000000000..c88ece3ea --- /dev/null +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -0,0 +1,109 @@ +import { + type Scheduler, + type SchedulerJob, + baseWatch, + onEffectCleanup, +} from '../src/baseWatch' +import { EffectScope } from '../src/effectScope' +import { type Ref, ref } from '../src/ref' + +const queue: SchedulerJob[] = [] + +let isFlushPending = false +const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise +const nextTick = (fn?: () => any) => + fn ? resolvedPromise.then(fn) : resolvedPromise +const scheduler: Scheduler = ({ job }) => { + queue.push(job) + flushJobs() +} +const flushJobs = () => { + if (isFlushPending) return + isFlushPending = true + resolvedPromise.then(() => { + queue.forEach(job => job()) + queue.length = 0 + isFlushPending = false + }) +} + +describe('baseWatch with onEffectCleanup', () => { + test('basic', async () => { + let dummy = 0 + let source: Ref + const scope = new EffectScope() + + scope.run(() => { + source = ref(0) + baseWatch(onCleanup => { + source.value + + onCleanup(() => (dummy += 2)) + onEffectCleanup(() => (dummy += 3)) + onEffectCleanup(() => (dummy += 5)) + }) + }) + expect(dummy).toBe(0) + + scope.run(() => { + source.value++ + }) + expect(dummy).toBe(10) + + scope.run(() => { + source.value++ + }) + expect(dummy).toBe(20) + + scope.stop() + expect(dummy).toBe(30) + }) + + test('nested call to baseWatch', async () => { + let calls: string[] = [] + let source: Ref + let copyist: Ref + const scope = new EffectScope() + + scope.run(() => { + source = ref(0) + copyist = ref(0) + // sync by default + baseWatch( + () => { + const current = (copyist.value = source.value) + onEffectCleanup(() => calls.push(`sync ${current}`)) + }, + null, + {}, + ) + // with scheduler + baseWatch( + () => { + const current = copyist.value + onEffectCleanup(() => calls.push(`post ${current}`)) + }, + null, + { scheduler }, + ) + }) + + await nextTick() + expect(calls).toEqual([]) + + scope.run(() => source.value++) + expect(calls).toEqual(['sync 0']) + await nextTick() + expect(calls).toEqual(['sync 0', 'post 0']) + calls.length = 0 + + scope.run(() => source.value++) + expect(calls).toEqual(['sync 1']) + await nextTick() + expect(calls).toEqual(['sync 1', 'post 1']) + calls.length = 0 + + scope.stop() + expect(calls).toEqual(['sync 2', 'post 2']) + }) +}) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index f51c00b37..caa6f2fff 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -116,7 +116,7 @@ export function onEffectCleanup(cleanupFn: () => void) { export function baseWatch( source: WatchSource | WatchSource[] | WatchEffect | object, - cb: WatchCallback | null, + cb?: WatchCallback | null, { immediate, deep, From d99e9a63839754bf5adc25aef805685753c8d947 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 28 Dec 2023 20:04:42 +0800 Subject: [PATCH 11/18] test: onEffectCleanup in runtime-core --- .../runtime-core/__tests__/apiWatch.spec.ts | 30 +++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 3bc614ef9..be19fde42 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -5,6 +5,7 @@ import { defineComponent, getCurrentInstance, nextTick, + onEffectCleanup, reactive, ref, watch, @@ -320,6 +321,35 @@ describe('api: watch', () => { expect(cleanup).toHaveBeenCalledTimes(2) }) + it('onEffectCleanup', async () => { + const count = ref(0) + const cleanupEffect = vi.fn() + const cleanupWatch = vi.fn() + + const stopEffect = watchEffect(() => { + onEffectCleanup(cleanupEffect) + count.value + }) + const stopWatch = watch(count, () => { + onEffectCleanup(cleanupWatch) + }) + + count.value++ + await nextTick() + expect(cleanupEffect).toHaveBeenCalledTimes(1) + expect(cleanupWatch).toHaveBeenCalledTimes(0) + + count.value++ + await nextTick() + expect(cleanupEffect).toHaveBeenCalledTimes(2) + expect(cleanupWatch).toHaveBeenCalledTimes(1) + + stopEffect() + expect(cleanupEffect).toHaveBeenCalledTimes(3) + stopWatch() + expect(cleanupWatch).toHaveBeenCalledTimes(2) + }) + it('flush timing: pre (default)', async () => { const count = ref(0) const count2 = ref(0) From e9555ce1f4bdffd8aef62cf7a1cde1fc8a365a52 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 28 Dec 2023 20:36:56 +0800 Subject: [PATCH 12/18] test: baseWatch --- .../reactivity/__tests__/baseWatch.spec.ts | 85 +++++++++++++++++-- 1 file changed, 77 insertions(+), 8 deletions(-) diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts index c88ece3ea..4b5a3dddd 100644 --- a/packages/reactivity/__tests__/baseWatch.spec.ts +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -1,14 +1,16 @@ +import type { Scheduler, SchedulerJob } from '../src/baseWatch' import { - type Scheduler, - type SchedulerJob, + BaseWatchErrorCodes, + EffectScope, + type Ref, baseWatch, onEffectCleanup, -} from '../src/baseWatch' -import { EffectScope } from '../src/effectScope' -import { type Ref, ref } from '../src/ref' + ref, +} from '../src/index' const queue: SchedulerJob[] = [] +// these codes are a simple scheduler let isFlushPending = false const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise const nextTick = (fn?: () => any) => @@ -27,8 +29,75 @@ const flushJobs = () => { }) } -describe('baseWatch with onEffectCleanup', () => { - test('basic', async () => { +describe('baseWatch', () => { + test('effect', () => { + let dummy: any + const source = ref(0) + baseWatch(() => { + dummy = source.value + }) + expect(dummy).toBe(0) + source.value++ + expect(dummy).toBe(1) + }) + + test('watch', () => { + let dummy: any + const source = ref(0) + baseWatch(source, () => { + dummy = source.value + }) + expect(dummy).toBe(undefined) + source.value++ + expect(dummy).toBe(1) + }) + + test('custom error handler', () => { + const handleError = vi.fn() + + baseWatch( + () => { + throw 'oops in effect' + }, + null, + { handleError }, + ) + + const source = ref(0) + const stop = baseWatch( + source, + () => { + onEffectCleanup(() => { + throw 'oops in cleanup' + }) + throw 'oops in watch' + }, + { handleError }, + ) + + expect(handleError.mock.calls.length).toBe(1) + expect(handleError.mock.calls[0]).toMatchObject([ + 'oops in effect', + BaseWatchErrorCodes.WATCH_CALLBACK, + ]) + + source.value++ + expect(handleError.mock.calls.length).toBe(2) + expect(handleError.mock.calls[1]).toMatchObject([ + 'oops in watch', + BaseWatchErrorCodes.WATCH_CALLBACK, + ]) + + stop() + source.value++ + expect(handleError.mock.calls.length).toBe(3) + expect(handleError.mock.calls[2]).toMatchObject([ + 'oops in cleanup', + BaseWatchErrorCodes.WATCH_CLEANUP, + ]) + }) + + test('baseWatch with onEffectCleanup', async () => { let dummy = 0 let source: Ref const scope = new EffectScope() @@ -59,7 +128,7 @@ describe('baseWatch with onEffectCleanup', () => { expect(dummy).toBe(30) }) - test('nested call to baseWatch', async () => { + test('nested calls to baseWatch and onEffectCleanup', async () => { let calls: string[] = [] let source: Ref let copyist: Ref From 90fd005a5aacec27d6253d09cd11eaed58a892cb Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 28 Dec 2023 21:05:03 +0800 Subject: [PATCH 13/18] chore: organize exports --- packages/reactivity/src/baseWatch.ts | 12 +++---- packages/reactivity/src/index.ts | 3 ++ packages/runtime-core/src/apiWatch.ts | 34 ------------------- packages/runtime-core/src/componentOptions.ts | 3 +- packages/runtime-core/src/directives.ts | 3 +- 5 files changed, 11 insertions(+), 44 deletions(-) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index caa6f2fff..5a04ed4ae 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -56,11 +56,11 @@ export interface SchedulerJob extends Function { allowRecurse?: boolean } -export type WatchEffect = (onCleanup: OnCleanup) => void +type WatchEffect = (onCleanup: OnCleanup) => void -export type WatchSource = Ref | ComputedRef | (() => T) +type WatchSource = Ref | ComputedRef | (() => T) -export type WatchCallback = ( +type WatchCallback = ( value: V, oldValue: OV, onCleanup: OnCleanup, @@ -77,7 +77,7 @@ export interface BaseWatchOptions extends DebuggerOptions { handleWarn?: HandleWarn } -export type WatchStopHandle = () => void +type WatchStopHandle = () => void export interface WatchInstance extends WatchStopHandle { effect?: ReactiveEffect @@ -369,7 +369,7 @@ export function traverse(value: unknown, seen?: Set) { return value } -export function callWithErrorHandling( +function callWithErrorHandling( fn: Function, handleError: HandleError, type: BaseWatchErrorCodes, @@ -384,7 +384,7 @@ export function callWithErrorHandling( return res } -export function callWithAsyncErrorHandling( +function callWithAsyncErrorHandling( fn: Function | Function[], handleError: HandleError, type: BaseWatchErrorCodes, diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index e69d7db0e..0f4dc53b5 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -73,5 +73,8 @@ export { baseWatch, onEffectCleanup, BaseWatchErrorCodes, + traverse, type BaseWatchOptions, + type Scheduler, + type WatchInstance, } from './baseWatch' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 051f305d9..08cdca362 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -3,11 +3,9 @@ import { type BaseWatchOptions, type ComputedRef, type DebuggerOptions, - ReactiveFlags, type Ref, baseWatch, getCurrentScope, - isRef, } from '@vue/reactivity' import { type SchedulerJob, @@ -18,12 +16,7 @@ import { EMPTY_OBJ, NOOP, extend, - isArray, isFunction, - isMap, - isObject, - isPlainObject, - isSet, isString, remove, } from '@vue/shared' @@ -284,30 +277,3 @@ export function createPathGetter(ctx: any, path: string) { return cur } } - -export function traverse(value: unknown, seen?: Set) { - if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { - return value - } - seen = seen || new Set() - if (seen.has(value)) { - return value - } - seen.add(value) - if (isRef(value)) { - traverse(value.value, seen) - } else if (isArray(value)) { - for (let i = 0; i < value.length; i++) { - traverse(value[i], seen) - } - } else if (isSet(value) || isMap(value)) { - value.forEach((v: any) => { - traverse(v, seen) - }) - } else if (isPlainObject(value)) { - for (const key in value) { - traverse(value[key], seen) - } - } - return value -} diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index 7214cca92..ef1f14063 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -19,13 +19,12 @@ import { isPromise, isString, } from '@vue/shared' -import { type Ref, getCurrentScope, isRef } from '@vue/reactivity' +import { type Ref, getCurrentScope, isRef, traverse } from '@vue/reactivity' import { computed } from './apiComputed' import { type WatchCallback, type WatchOptions, createPathGetter, - traverse, watch, } from './apiWatch' import { inject, provide } from './apiInject' diff --git a/packages/runtime-core/src/directives.ts b/packages/runtime-core/src/directives.ts index a3b44d4c8..e3e7222ff 100644 --- a/packages/runtime-core/src/directives.ts +++ b/packages/runtime-core/src/directives.ts @@ -23,8 +23,7 @@ import { currentRenderingInstance } from './componentRenderContext' import { ErrorCodes, callWithAsyncErrorHandling } from './errorHandling' import type { ComponentPublicInstance } from './componentPublicInstance' import { mapCompatDirectiveHook } from './compat/customDirective' -import { pauseTracking, resetTracking } from '@vue/reactivity' -import { traverse } from './apiWatch' +import { pauseTracking, resetTracking, traverse } from '@vue/reactivity' export interface DirectiveBinding { instance: ComponentPublicInstance | null From a078ad11c4cd865318c70493d261922fdba974c4 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 28 Dec 2023 21:28:28 +0800 Subject: [PATCH 14/18] chore: rename handleWarn to onWarn --- .../reactivity/__tests__/baseWatch.spec.ts | 18 ++++++------ packages/reactivity/src/baseWatch.ts | 28 ++++++++----------- packages/runtime-core/src/apiWatch.ts | 4 +-- 3 files changed, 23 insertions(+), 27 deletions(-) diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts index 4b5a3dddd..53db9522b 100644 --- a/packages/reactivity/__tests__/baseWatch.spec.ts +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -53,14 +53,14 @@ describe('baseWatch', () => { }) test('custom error handler', () => { - const handleError = vi.fn() + const onError = vi.fn() baseWatch( () => { throw 'oops in effect' }, null, - { handleError }, + { onError }, ) const source = ref(0) @@ -72,26 +72,26 @@ describe('baseWatch', () => { }) throw 'oops in watch' }, - { handleError }, + { onError }, ) - expect(handleError.mock.calls.length).toBe(1) - expect(handleError.mock.calls[0]).toMatchObject([ + expect(onError.mock.calls.length).toBe(1) + expect(onError.mock.calls[0]).toMatchObject([ 'oops in effect', BaseWatchErrorCodes.WATCH_CALLBACK, ]) source.value++ - expect(handleError.mock.calls.length).toBe(2) - expect(handleError.mock.calls[1]).toMatchObject([ + expect(onError.mock.calls.length).toBe(2) + expect(onError.mock.calls[1]).toMatchObject([ 'oops in watch', BaseWatchErrorCodes.WATCH_CALLBACK, ]) stop() source.value++ - expect(handleError.mock.calls.length).toBe(3) - expect(handleError.mock.calls[2]).toMatchObject([ + expect(onError.mock.calls.length).toBe(3) + expect(onError.mock.calls[2]).toMatchObject([ 'oops in cleanup', BaseWatchErrorCodes.WATCH_CLEANUP, ]) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 5a04ed4ae..72679633e 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -73,8 +73,8 @@ export interface BaseWatchOptions extends DebuggerOptions { deep?: boolean once?: boolean scheduler?: Scheduler - handleError?: HandleError - handleWarn?: HandleWarn + onError?: HandleError + onWarn?: HandleWarn } type WatchStopHandle = () => void @@ -121,15 +121,15 @@ export function baseWatch( immediate, deep, once, + scheduler = DEFAULT_SCHEDULER, + onWarn = __DEV__ ? warn : NOOP, + onError = DEFAULT_HANDLE_ERROR, onTrack, onTrigger, - scheduler = DEFAULT_SCHEDULER, - handleError: handleError = DEFAULT_HANDLE_ERROR, - handleWarn: handleWarn = __DEV__ ? warn : NOOP, }: BaseWatchOptions = EMPTY_OBJ, ): WatchInstance { const warnInvalidSource = (s: unknown) => { - handleWarn( + onWarn( `Invalid watch source: `, s, `A watch source can only be a getter/effect function, a ref, ` + @@ -161,7 +161,7 @@ export function baseWatch( } else if (isFunction(s)) { return callWithErrorHandling( s, - handleError, + onError, BaseWatchErrorCodes.WATCH_GETTER, ) } else { @@ -172,11 +172,7 @@ export function baseWatch( if (cb) { // getter with cb getter = () => - callWithErrorHandling( - source, - handleError, - BaseWatchErrorCodes.WATCH_GETTER, - ) + callWithErrorHandling(source, onError, BaseWatchErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect getter = () => { @@ -193,7 +189,7 @@ export function baseWatch( try { return callWithAsyncErrorHandling( source, - handleError, + onError, BaseWatchErrorCodes.WATCH_CALLBACK, [onEffectCleanup], ) @@ -224,7 +220,7 @@ export function baseWatch( getCurrentScope()?.effects.push((effect = {} as any)) callWithAsyncErrorHandling( cb, - handleError, + onError, BaseWatchErrorCodes.WATCH_CALLBACK, [getter(), isMultiSource ? [] : undefined, onEffectCleanup], ) @@ -263,7 +259,7 @@ export function baseWatch( try { callWithAsyncErrorHandling( cb, - handleError, + onError, BaseWatchErrorCodes.WATCH_CALLBACK, [ newValue, @@ -306,7 +302,7 @@ export function baseWatch( cleanups.forEach(cleanup => callWithErrorHandling( cleanup, - handleError, + onError, BaseWatchErrorCodes.WATCH_CLEANUP, ), ) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 08cdca362..e5f588918 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -198,7 +198,7 @@ function doWatch( const extendOptions: BaseWatchOptions = {} - if (__DEV__) extendOptions.handleWarn = warn + if (__DEV__) extendOptions.onWarn = warn let ssrCleanup: (() => void)[] | undefined if (__SSR__ && isInSSRComponentSetup) { @@ -217,7 +217,7 @@ function doWatch( const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null - extendOptions.handleError = (err: unknown, type: BaseWatchErrorCodes) => + extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => handleErrorWithInstance(err, instance, type) const scheduler = getSchedulerByFlushMode(flush)({ instance }) From f44ef0b74b325b19cac6ec0fd8c17b03c6ae0337 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sun, 31 Dec 2023 18:45:04 +0800 Subject: [PATCH 15/18] feat: implement getCurrentEffect --- packages/reactivity/src/baseWatch.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 72679633e..a7c847b36 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -105,12 +105,31 @@ export type HandleWarn = (msg: string, ...args: any[]) => void const cleanupMap: WeakMap void)[]> = new WeakMap() let activeEffect: ReactiveEffect | undefined = undefined +/** + * Returns the current active effect if there is one. + */ +export function getCurrentEffect() { + return activeEffect +} + +/** + * Registers a cleanup callback on the current active effect. This + * registered cleanup callback will be invoked right before the + * associated effect re-runs. + * + * @param cleanupFn - The callback function to attach to the effect's cleanup. + */ export function onEffectCleanup(cleanupFn: () => void) { if (activeEffect) { const cleanups = cleanupMap.get(activeEffect) || cleanupMap.set(activeEffect, []).get(activeEffect)! cleanups.push(cleanupFn) + } else if (__DEV__) { + warn( + `onEffectCleanup() was called when there was no active effect` + + ` to associate with.`, + ) } } From 2213634269ddc4574d639f732d2611caf0b8c043 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Sun, 31 Dec 2023 19:21:12 +0800 Subject: [PATCH 16/18] refactor: simplify unwatch implementation --- .../reactivity/__tests__/baseWatch.spec.ts | 4 ++-- packages/reactivity/src/baseWatch.ts | 21 +++++-------------- packages/reactivity/src/index.ts | 1 - packages/runtime-core/src/apiWatch.ts | 16 +++++++------- 4 files changed, 16 insertions(+), 26 deletions(-) diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts index 53db9522b..329ebaacb 100644 --- a/packages/reactivity/__tests__/baseWatch.spec.ts +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -64,7 +64,7 @@ describe('baseWatch', () => { ) const source = ref(0) - const stop = baseWatch( + const effect = baseWatch( source, () => { onEffectCleanup(() => { @@ -88,7 +88,7 @@ describe('baseWatch', () => { BaseWatchErrorCodes.WATCH_CALLBACK, ]) - stop() + effect!.stop() source.value++ expect(onError.mock.calls.length).toBe(3) expect(onError.mock.calls[2]).toMatchObject([ diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index a7c847b36..003a2620f 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -77,12 +77,6 @@ export interface BaseWatchOptions extends DebuggerOptions { onWarn?: HandleWarn } -type WatchStopHandle = () => void - -export interface WatchInstance extends WatchStopHandle { - effect?: ReactiveEffect -} - // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} @@ -146,7 +140,7 @@ export function baseWatch( onTrack, onTrigger, }: BaseWatchOptions = EMPTY_OBJ, -): WatchInstance { +): ReactiveEffect | undefined { const warnInvalidSource = (s: unknown) => { onWarn( `Invalid watch source: `, @@ -232,7 +226,7 @@ export function baseWatch( // onEffectCleanup need use effect as a key getCurrentScope()?.effects.push((effect = {} as any)) getter() - return NOOP + return } if (immediate) { // onEffectCleanup need use effect as a key @@ -243,12 +237,12 @@ export function baseWatch( BaseWatchErrorCodes.WATCH_CALLBACK, [getter(), isMultiSource ? [] : undefined, onEffectCleanup], ) - return NOOP + return } const _cb = cb cb = (...args) => { _cb(...args) - unwatch() + effect?.stop() } } @@ -329,11 +323,6 @@ export function baseWatch( } } - const unwatch: WatchInstance = () => { - effect.stop() - } - unwatch.effect = effect - if (__DEV__) { effect.onTrack = onTrack effect.onTrigger = onTrigger @@ -354,7 +343,7 @@ export function baseWatch( }) } - return unwatch + return effect } export function traverse(value: unknown, seen?: Set) { diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 0f4dc53b5..6e2e51875 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -76,5 +76,4 @@ export { traverse, type BaseWatchOptions, type Scheduler, - type WatchInstance, } from './baseWatch' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index e5f588918..c48a4e9c8 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -223,14 +223,16 @@ function doWatch( const scheduler = getSchedulerByFlushMode(flush)({ instance }) extendOptions.scheduler = scheduler - let baseUnwatch = baseWatch(source, cb, extend({}, options, extendOptions)) + let effect = baseWatch(source, cb, extend({}, options, extendOptions)) - const unwatch = () => { - baseUnwatch() - if (instance && instance.scope) { - remove(instance.scope.effects!, baseUnwatch.effect) - } - } + const unwatch = !effect + ? NOOP + : () => { + effect!.stop() + if (instance && instance.scope) { + remove(instance.scope.effects!, effect) + } + } if (__SSR__ && ssrCleanup) ssrCleanup.push(unwatch) return unwatch From 58ceb753c256f725da90781acb2c7860b4803640 Mon Sep 17 00:00:00 2001 From: Rizumu Ayaka Date: Thu, 4 Jan 2024 22:38:03 +0800 Subject: [PATCH 17/18] fix: modify the code according to the review --- .../reactivity/__tests__/baseWatch.spec.ts | 2 +- packages/reactivity/src/baseWatch.ts | 36 ++++++++----------- packages/runtime-core/src/apiWatch.ts | 18 +++++----- packages/runtime-core/src/errorHandling.ts | 8 ++++- packages/runtime-core/src/renderer.ts | 7 ++-- packages/runtime-core/src/scheduler.ts | 24 +++++-------- 6 files changed, 43 insertions(+), 52 deletions(-) diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts index 329ebaacb..6fa29ade0 100644 --- a/packages/reactivity/__tests__/baseWatch.spec.ts +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -15,7 +15,7 @@ let isFlushPending = false const resolvedPromise = /*#__PURE__*/ Promise.resolve() as Promise const nextTick = (fn?: () => any) => fn ? resolvedPromise.then(fn) : resolvedPromise -const scheduler: Scheduler = ({ job }) => { +const scheduler: Scheduler = job => { queue.push(job) flushJobs() } diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 8afd74247..4f47809ef 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -24,12 +24,13 @@ import { isReactive, isShallow } from './reactive' import { type Ref, isRef } from './ref' import { getCurrentScope } from './effectScope' -// contexts where user provided function may be executed, in addition to -// lifecycle hooks. +// These errors were transferred from `packages/runtime-core/src/errorHandling.ts` +// along with baseWatch to maintain code compatibility. Hence, +// it is essential to keep these values unchanged. export enum BaseWatchErrorCodes { - WATCH_GETTER = 'BaseWatchErrorCodes_WATCH_GETTER', - WATCH_CALLBACK = 'BaseWatchErrorCodes_WATCH_CALLBACK', - WATCH_CLEANUP = 'BaseWatchErrorCodes_WATCH_CLEANUP', + WATCH_GETTER = 2, + WATCH_CALLBACK, + WATCH_CLEANUP, } // TODO move to a scheduler package @@ -80,13 +81,13 @@ export interface BaseWatchOptions extends DebuggerOptions { // initial value for watchers to trigger on undefined initial values const INITIAL_WATCHER_VALUE = {} -export type Scheduler = (context: { - effect: ReactiveEffect - job: SchedulerJob - isInit: boolean -}) => void +export type Scheduler = ( + job: SchedulerJob, + effect: ReactiveEffect, + isInit: boolean, +) => void -const DEFAULT_SCHEDULER: Scheduler = ({ job }) => job() +const DEFAULT_SCHEDULER: Scheduler = job => job() export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void @@ -306,12 +307,7 @@ export function baseWatch( // it is allowed to self-trigger (#1727) job.allowRecurse = !!cb - let effectScheduler: EffectScheduler = () => - scheduler({ - effect, - job, - isInit: false, - }) + let effectScheduler: EffectScheduler = () => scheduler(job, effect, false) effect = new ReactiveEffect(getter, NOOP, effectScheduler) @@ -342,11 +338,7 @@ export function baseWatch( oldValue = effect.run() } } else { - scheduler({ - effect, - job, - isInit: true, - }) + scheduler(job, effect, true) } return effect diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 0371dc57c..8535b262c 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -8,9 +8,9 @@ import { getCurrentScope, } from '@vue/reactivity' import { - type SchedulerJob, - usePreScheduler, - useSyncScheduler, + type CreateScheduler, + createPreScheduler, + createSyncScheduler, } from './scheduler' import { EMPTY_OBJ, @@ -28,7 +28,7 @@ import { unsetCurrentInstance, } from './component' import { handleError as handleErrorWithInstance } from './errorHandling' -import { usePostRenderScheduler } from './renderer' +import { createPostRenderScheduler } from './renderer' import { warn } from './warning' import type { ObjectWatchOptionItem } from './componentOptions' import { useSSRContext } from './helpers/useSsrContext' @@ -158,15 +158,15 @@ export function watch = false>( function getSchedulerByFlushMode( flush: WatchOptionsBase['flush'], -): SchedulerJob { +): CreateScheduler { if (flush === 'post') { - return usePostRenderScheduler + return createPostRenderScheduler } if (flush === 'sync') { - return useSyncScheduler + return createSyncScheduler } // default: 'pre' - return usePreScheduler + return createPreScheduler } function doWatch( @@ -228,7 +228,7 @@ function doWatch( extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => handleErrorWithInstance(err, instance, type) - const scheduler = getSchedulerByFlushMode(flush)({ instance }) + const scheduler = getSchedulerByFlushMode(flush)(instance) extendOptions.scheduler = scheduler let effect = baseWatch(source, cb, extend({}, options, extendOptions)) diff --git a/packages/runtime-core/src/errorHandling.ts b/packages/runtime-core/src/errorHandling.ts index 82de575c1..8686aee38 100644 --- a/packages/runtime-core/src/errorHandling.ts +++ b/packages/runtime-core/src/errorHandling.ts @@ -10,7 +10,13 @@ import { BaseWatchErrorCodes } from '@vue/reactivity' export enum ErrorCodes { SETUP_FUNCTION, RENDER_FUNCTION, - NATIVE_EVENT_HANDLER, + // The error codes for the watch have been transferred to the reactivity + // package along with baseWatch to maintain code compatibility. Hence, + // it is essential to keep these values unchanged. + // WATCH_GETTER, + // WATCH_CALLBACK, + // WATCH_CLEANUP, + NATIVE_EVENT_HANDLER = 5, COMPONENT_EVENT_HANDLER, VNODE_HOOK, DIRECTIVE_HOOK, diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 27ed5ee02..f80d3c438 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -38,7 +38,7 @@ import { isReservedProp, } from '@vue/shared' import { - type Scheduler, + type CreateScheduler, type SchedulerJob, flushPostFlushCbs, flushPreFlushCbs, @@ -282,9 +282,8 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__ : queueEffectWithSuspense : queuePostFlushCb -export const usePostRenderScheduler: Scheduler = - ({ instance }) => - ({ isInit, effect, job }) => { +export const createPostRenderScheduler: CreateScheduler = + instance => (job, effect, isInit) => { if (isInit) { queuePostRenderEffect( effect.run.bind(effect), diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 5b15a8ae6..8cc2db29a 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -1,7 +1,7 @@ import { ErrorCodes, callWithErrorHandling, handleError } from './errorHandling' import { type Awaited, NOOP, isArray } from '@vue/shared' import { type ComponentInternalInstance, getComponentName } from './component' -import type { ReactiveEffect } from '@vue/reactivity' +import type { Scheduler } from '@vue/reactivity' export interface SchedulerJob extends Function { id?: number @@ -289,17 +289,12 @@ 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 }) => { +export type CreateScheduler = ( + instance: ComponentInternalInstance | null, +) => Scheduler + +export const createSyncScheduler: CreateScheduler = + instance => (job, effect, isInit) => { if (isInit) { effect.run() } else { @@ -307,9 +302,8 @@ export const useSyncScheduler: Scheduler = } } -export const usePreScheduler: Scheduler = - ({ instance }) => - ({ isInit, effect, job }) => { +export const createPreScheduler: CreateScheduler = + instance => (job, effect, isInit) => { if (isInit) { effect.run() } else { From c1b3e7c5dbbb7e16bc21d4e9ab27212f87726f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=B8=89=E5=92=B2=E6=99=BA=E5=AD=90=20Kevin=20Deng?= Date: Thu, 4 Jan 2024 23:10:02 +0800 Subject: [PATCH 18/18] chore: update --- packages/reactivity/__tests__/baseWatch.spec.ts | 2 +- packages/reactivity/src/baseWatch.ts | 10 ++-------- packages/reactivity/src/index.ts | 2 +- packages/runtime-core/src/apiWatch.ts | 11 +++-------- packages/runtime-core/src/componentOptions.ts | 2 +- packages/runtime-core/src/renderer.ts | 4 ++-- packages/runtime-core/src/scheduler.ts | 6 +++--- 7 files changed, 13 insertions(+), 24 deletions(-) diff --git a/packages/reactivity/__tests__/baseWatch.spec.ts b/packages/reactivity/__tests__/baseWatch.spec.ts index 6fa29ade0..02d9e64e0 100644 --- a/packages/reactivity/__tests__/baseWatch.spec.ts +++ b/packages/reactivity/__tests__/baseWatch.spec.ts @@ -6,7 +6,7 @@ import { baseWatch, onEffectCleanup, ref, -} from '../src/index' +} from '../src' const queue: SchedulerJob[] = [] diff --git a/packages/reactivity/src/baseWatch.ts b/packages/reactivity/src/baseWatch.ts index 4f47809ef..a97f43366 100644 --- a/packages/reactivity/src/baseWatch.ts +++ b/packages/reactivity/src/baseWatch.ts @@ -58,15 +58,12 @@ export interface SchedulerJob extends Function { } type WatchEffect = (onCleanup: OnCleanup) => void - type WatchSource = Ref | ComputedRef | (() => T) - type WatchCallback = ( value: V, oldValue: OV, onCleanup: OnCleanup, ) => any - type OnCleanup = (cleanupFn: () => void) => void export interface BaseWatchOptions extends DebuggerOptions { @@ -86,17 +83,14 @@ export type Scheduler = ( effect: ReactiveEffect, isInit: boolean, ) => void - -const DEFAULT_SCHEDULER: Scheduler = job => job() - export type HandleError = (err: unknown, type: BaseWatchErrorCodes) => void +export type HandleWarn = (msg: string, ...args: any[]) => void +const DEFAULT_SCHEDULER: Scheduler = job => job() const DEFAULT_HANDLE_ERROR: HandleError = (err: unknown) => { throw err } -export type HandleWarn = (msg: string, ...args: any[]) => void - const cleanupMap: WeakMap void)[]> = new WeakMap() let activeEffect: ReactiveEffect | undefined = undefined diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 6e2e51875..a8db2454a 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -72,8 +72,8 @@ export { TrackOpTypes, TriggerOpTypes, ReactiveFlags } from './constants' export { baseWatch, onEffectCleanup, - BaseWatchErrorCodes, traverse, + BaseWatchErrorCodes, type BaseWatchOptions, type Scheduler, } from './baseWatch' diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 8535b262c..b6c0d5f83 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -8,7 +8,7 @@ import { getCurrentScope, } from '@vue/reactivity' import { - type CreateScheduler, + type SchedulerFactory, createPreScheduler, createSyncScheduler, } from './scheduler' @@ -156,9 +156,7 @@ export function watch = false>( return doWatch(source as any, cb, options) } -function getSchedulerByFlushMode( - flush: WatchOptionsBase['flush'], -): CreateScheduler { +function getScheduler(flush: WatchOptionsBase['flush']): SchedulerFactory { if (flush === 'post') { return createPostRenderScheduler } @@ -224,12 +222,9 @@ function doWatch( } const instance = currentInstance - extendOptions.onError = (err: unknown, type: BaseWatchErrorCodes) => handleErrorWithInstance(err, instance, type) - - const scheduler = getSchedulerByFlushMode(flush)(instance) - extendOptions.scheduler = scheduler + extendOptions.scheduler = getScheduler(flush)(instance) let effect = baseWatch(source, cb, extend({}, options, extendOptions)) diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index cfeb61118..1017f8b64 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -942,7 +942,7 @@ export function createWatcher( ? createPathGetter(publicThis, key) : () => (publicThis as any)[key] - const options: WatchOptions = {} + const options: WatchOptions = {} if (__COMPAT__) { const instance = getCurrentScope() === currentInstance?.scope ? currentInstance : null diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index f80d3c438..08762084e 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -38,7 +38,7 @@ import { isReservedProp, } from '@vue/shared' import { - type CreateScheduler, + type SchedulerFactory, type SchedulerJob, flushPostFlushCbs, flushPreFlushCbs, @@ -282,7 +282,7 @@ export const queuePostRenderEffect = __FEATURE_SUSPENSE__ : queueEffectWithSuspense : queuePostFlushCb -export const createPostRenderScheduler: CreateScheduler = +export const createPostRenderScheduler: SchedulerFactory = instance => (job, effect, isInit) => { if (isInit) { queuePostRenderEffect( diff --git a/packages/runtime-core/src/scheduler.ts b/packages/runtime-core/src/scheduler.ts index 8cc2db29a..2ab47e51d 100644 --- a/packages/runtime-core/src/scheduler.ts +++ b/packages/runtime-core/src/scheduler.ts @@ -289,11 +289,11 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) { } } -export type CreateScheduler = ( +export type SchedulerFactory = ( instance: ComponentInternalInstance | null, ) => Scheduler -export const createSyncScheduler: CreateScheduler = +export const createSyncScheduler: SchedulerFactory = instance => (job, effect, isInit) => { if (isInit) { effect.run() @@ -302,7 +302,7 @@ export const createSyncScheduler: CreateScheduler = } } -export const createPreScheduler: CreateScheduler = +export const createPreScheduler: SchedulerFactory = instance => (job, effect, isInit) => { if (isInit) { effect.run()