diff --git a/src/v3/apiWatch.ts b/src/v3/apiWatch.ts index 7160009dabb..51617e39a1b 100644 --- a/src/v3/apiWatch.ts +++ b/src/v3/apiWatch.ts @@ -53,6 +53,7 @@ export interface WatchOptionsBase extends DebuggerOptions { export interface WatchOptions extends WatchOptionsBase { immediate?: Immediate deep?: boolean + equals?: (value: any, oldValue: any) => boolean } export type WatchStopHandle = () => void @@ -159,7 +160,8 @@ function doWatch( deep, flush = 'pre', onTrack, - onTrigger + onTrigger, + equals }: WatchOptions = emptyObject ): WatchStopHandle { if (__DEV__ && !cb) { @@ -175,6 +177,17 @@ function doWatch( `watch(source, callback, options?) signature.` ) } + if (equals !== undefined) { + warn( + `watch() "equals" option is only respected when using the ` + + `watch(source, callback, options?) signature.` + ) + } + } + + const equalsFn = isFunction(equals) ? equals : undefined + if (__DEV__ && equals !== undefined && !equalsFn) { + warn(`watch() "equals" option must be a function.`) } const warnInvalidSource = (s: unknown) => { @@ -275,7 +288,18 @@ function doWatch( }) watcher.noRecurse = !cb - let oldValue = isMultiSource ? [] : INITIAL_WATCHER_VALUE + let oldValue: any = INITIAL_WATCHER_VALUE + const hasChangedValue = (newValue: any, oldValue: any) => { + if (equalsFn) { + return !equalsFn(newValue, oldValue) + } + if (isMultiSource) { + return (newValue as any[]).some((v, i) => + hasChanged(v, (oldValue as any[])[i]) + ) + } + return hasChanged(newValue, oldValue) + } // overwrite default run watcher.run = () => { if (!watcher.active) { @@ -284,14 +308,12 @@ function doWatch( if (cb) { // watch(source, cb) const newValue = watcher.get() + const isInitialValue = oldValue === INITIAL_WATCHER_VALUE if ( - deep || - forceTrigger || - (isMultiSource - ? (newValue as any[]).some((v, i) => - hasChanged(v, (oldValue as any[])[i]) - ) - : hasChanged(newValue, oldValue)) + isInitialValue || + (equalsFn + ? hasChangedValue(newValue, oldValue) + : deep || forceTrigger || hasChangedValue(newValue, oldValue)) ) { // cleanup before running cb again if (cleanup) { @@ -300,7 +322,7 @@ function doWatch( call(cb, WATCHER_CB, [ newValue, // pass undefined as the old value when it's changed for the first time - oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, + isInitialValue ? (isMultiSource ? [] : undefined) : oldValue, onCleanup ]) oldValue = newValue diff --git a/test/unit/features/v3/apiWatch.spec.ts b/test/unit/features/v3/apiWatch.spec.ts index a684d16116e..e8c58f0c830 100644 --- a/test/unit/features/v3/apiWatch.spec.ts +++ b/test/unit/features/v3/apiWatch.spec.ts @@ -305,6 +305,39 @@ describe('api: watch', () => { expect(cleanup).toHaveBeenCalledTimes(2) }) + it('respects equals to skip cleanup when values are equivalent', async () => { + const state = ref({ count: 0 }) + const cleanup = vi.fn() + const cb = vi.fn((_value, _oldValue, onCleanup) => { + onCleanup(cleanup) + }) + + watch(state, cb, { + deep: true, + equals: (value, oldValue) => value.count === oldValue.count + }) + + state.value = { count: 0 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(0) + expect(cleanup).toHaveBeenCalledTimes(0) + + state.value = { count: 1 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + expect(cleanup).toHaveBeenCalledTimes(0) + + state.value = { count: 1 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(1) + expect(cleanup).toHaveBeenCalledTimes(0) + + state.value = { count: 2 } + await nextTick() + expect(cb).toHaveBeenCalledTimes(2) + expect(cleanup).toHaveBeenCalledTimes(1) + }) + it('flush timing: pre (default)', async () => { const count = ref(0) const count2 = ref(0)