diff --git a/packages/reactivity/__tests__/watch.spec.ts b/packages/reactivity/__tests__/watch.spec.ts index 245acfd63be..3333f1cb8c0 100644 --- a/packages/reactivity/__tests__/watch.spec.ts +++ b/packages/reactivity/__tests__/watch.spec.ts @@ -6,6 +6,7 @@ import { type WatchScheduler, computed, onWatcherCleanup, + reactive, ref, watch, } from '../src' @@ -211,6 +212,27 @@ describe('watch', () => { expect(dummy).toBe(1) }) + test('skip option', async () => { + let dummy = 0 + const source = reactive({ + num1: 1, + nested: { _skip: true, num2: 0 }, + }) + watch(source, () => dummy++, { + deep: true, + skip(val) { + return val._skip + }, + }) + expect(dummy).toBe(0) + source.nested.num2++ + expect(dummy).toBe(0) + source.num1++ + expect(dummy).toBe(1) + source.nested.num2++ + expect(dummy).toBe(1) + }) + // #12033 test('recursive sync watcher on computed', () => { const r = ref(0) diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index f0445e87da0..e435c318994 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -93,5 +93,6 @@ export { type WatchEffect, type WatchSource, type WatchCallback, + type WatchSkip, type OnCleanup, } from './watch' diff --git a/packages/reactivity/src/watch.ts b/packages/reactivity/src/watch.ts index 073bf88b93f..5d44075b877 100644 --- a/packages/reactivity/src/watch.ts +++ b/packages/reactivity/src/watch.ts @@ -49,6 +49,7 @@ export type OnCleanup = (cleanupFn: () => void) => void export interface WatchOptions extends DebuggerOptions { immediate?: Immediate deep?: boolean | number + skip?: WatchSkip once?: boolean scheduler?: WatchScheduler onWarn?: (msg: string, ...args: any[]) => void @@ -79,6 +80,8 @@ const INITIAL_WATCHER_VALUE = {} export type WatchScheduler = (job: () => void, isFirstRun: boolean) => void +export type WatchSkip = (val: any) => boolean | undefined + const cleanupMap: WeakMap void)[]> = new WeakMap() let activeWatcher: ReactiveEffect | undefined = undefined @@ -122,7 +125,7 @@ export function watch( cb?: WatchCallback | null, options: WatchOptions = EMPTY_OBJ, ): WatchHandle { - const { immediate, deep, once, scheduler, augmentJob, call } = options + const { immediate, deep, skip, once, scheduler, augmentJob, call } = options const warnInvalidSource = (s: unknown) => { ;(options.onWarn || warn)( @@ -138,9 +141,9 @@ export function watch( if (deep) return source // for `deep: false | 0` or shallow reactive, only traverse root-level properties if (isShallow(source) || deep === false || deep === 0) - return traverse(source, 1) + return traverse(source, 1, skip) // for `deep: undefined` on a reactive object, deeply traverse all properties - return traverse(source) + return traverse(source, Infinity, skip) } let effect: ReactiveEffect @@ -207,7 +210,7 @@ export function watch( if (cb && deep) { const baseGetter = getter const depth = deep === true ? Infinity : deep - getter = () => traverse(baseGetter(), depth) + getter = () => traverse(baseGetter(), depth, skip) } const scope = getCurrentScope() @@ -331,12 +334,18 @@ export function watch( export function traverse( value: unknown, depth: number = Infinity, + skip?: WatchSkip, seen?: Set, ): unknown { if (depth <= 0 || !isObject(value) || (value as any)[ReactiveFlags.SKIP]) { return value } - + if (skip) { + pauseTracking() + const res = skip(value) + resetTracking() + if (res) return value + } seen = seen || new Set() if (seen.has(value)) { return value @@ -344,22 +353,22 @@ export function traverse( seen.add(value) depth-- if (isRef(value)) { - traverse(value.value, depth, seen) + traverse(value.value, depth, skip, seen) } else if (isArray(value)) { for (let i = 0; i < value.length; i++) { - traverse(value[i], depth, seen) + traverse(value[i], depth, skip, seen) } } else if (isSet(value) || isMap(value)) { value.forEach((v: any) => { - traverse(v, depth, seen) + traverse(v, depth, skip, seen) }) } else if (isPlainObject(value)) { for (const key in value) { - traverse(value[key], depth, seen) + traverse(value[key], depth, skip, seen) } for (const key of Object.getOwnPropertySymbols(value)) { if (Object.prototype.propertyIsEnumerable.call(value, key)) { - traverse(value[key as any], depth, seen) + traverse(value[key as any], depth, skip, seen) } } } diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 8f6168cdf29..812cc746ab3 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -5,6 +5,7 @@ import { type WatchCallback, type WatchEffect, type WatchHandle, + type WatchSkip, type WatchSource, watch as baseWatch, } from '@vue/reactivity' @@ -48,6 +49,7 @@ export interface WatchEffectOptions extends DebuggerOptions { export interface WatchOptions extends WatchEffectOptions { immediate?: Immediate deep?: boolean | number + skip?: WatchSkip once?: boolean } @@ -143,7 +145,7 @@ function doWatch( cb: WatchCallback | null, options: WatchOptions = EMPTY_OBJ, ): WatchHandle { - const { immediate, deep, flush, once } = options + const { immediate, deep, skip: skip, flush, once } = options if (__DEV__ && !cb) { if (immediate !== undefined) { @@ -158,6 +160,12 @@ function doWatch( `watch(source, callback, options?) signature.`, ) } + if (skip !== undefined) { + warn( + `watch() "skip" option is only respected when using the ` + + `watch(source, callback, options?) signature.`, + ) + } if (once !== undefined) { warn( `watch() "once" option is only respected when using the ` +