Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 32 additions & 10 deletions src/v3/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ export interface WatchOptionsBase extends DebuggerOptions {
export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {
immediate?: Immediate
deep?: boolean
equals?: (value: any, oldValue: any) => boolean
}

export type WatchStopHandle = () => void
Expand Down Expand Up @@ -159,7 +160,8 @@ function doWatch(
deep,
flush = 'pre',
onTrack,
onTrigger
onTrigger,
equals
}: WatchOptions = emptyObject
): WatchStopHandle {
if (__DEV__ && !cb) {
Expand All @@ -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) => {
Expand Down Expand Up @@ -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) {
Expand All @@ -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) {
Expand All @@ -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
Expand Down
33 changes: 33 additions & 0 deletions test/unit/features/v3/apiWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down