diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 3bc614ef9b7..b25635e664e 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -25,9 +25,11 @@ import { type DebuggerEvent, ITERATE_KEY, type Ref, + type ShallowRef, TrackOpTypes, TriggerOpTypes, effectScope, + shallowReactive, shallowRef, toRef, triggerRef, @@ -156,6 +158,59 @@ describe('api: watch', () => { expect(dummy).toBe(1) }) + it('directly watching reactive object with explicit deep: false', async () => { + const src = reactive({ + state: { + count: 0, + }, + }) + let dummy + watch( + src, + ({ state }) => { + dummy = state?.count + }, + { + deep: false, + }, + ) + + // nested should not trigger + src.state.count++ + await nextTick() + expect(dummy).toBe(undefined) + + // root level should trigger + src.state = { count: 1 } + await nextTick() + expect(dummy).toBe(1) + }) + + // #9916 + it('directly watching shallow reactive array', async () => { + class foo { + prop1: ShallowRef = shallowRef('') + prop2: string = '' + } + + const obj1 = new foo() + const obj2 = new foo() + + const collection = shallowReactive([obj1, obj2]) + const cb = vi.fn() + watch(collection, cb) + + collection[0].prop1.value = 'foo' + await nextTick() + // should not trigger + expect(cb).toBeCalledTimes(0) + + collection.push(new foo()) + await nextTick() + // should trigger on array self mutation + expect(cb).toBeCalledTimes(1) + }) + it('watching multiple sources', async () => { const state = reactive({ count: 1 }) const count = ref(1) diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index a3cd3894f41..0c13e72988f 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -231,8 +231,11 @@ function doWatch( getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { - getter = () => source - deep = true + getter = + isShallow(source) || deep === false + ? () => traverse(source, 1) + : () => traverse(source) + forceTrigger = true } else if (isArray(source)) { isMultiSource = true forceTrigger = source.some(s => isReactive(s) || isShallow(s)) @@ -241,7 +244,7 @@ function doWatch( if (isRef(s)) { return s.value } else if (isReactive(s)) { - return traverse(s) + return traverse(s, isShallow(s) || deep === false ? 1 : undefined) } else if (isFunction(s)) { return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { @@ -460,28 +463,41 @@ export function createPathGetter(ctx: any, path: string) { } } -export function traverse(value: unknown, seen?: Set) { +export function traverse( + value: unknown, + depth?: number, + currentDepth = 0, + seen?: Set, +) { if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { return value } + + if (depth && depth > 0) { + if (currentDepth >= depth) { + return value + } + currentDepth++ + } + seen = seen || new Set() if (seen.has(value)) { return value } seen.add(value) if (isRef(value)) { - traverse(value.value, seen) + traverse(value.value, depth, currentDepth, seen) } else if (isArray(value)) { for (let i = 0; i < value.length; i++) { - traverse(value[i], seen) + traverse(value[i], depth, currentDepth, seen) } } else if (isSet(value) || isMap(value)) { value.forEach((v: any) => { - traverse(v, seen) + traverse(v, depth, currentDepth, seen) }) } else if (isPlainObject(value)) { for (const key in value) { - traverse(value[key], seen) + traverse(value[key], depth, currentDepth, seen) } } return value