diff --git a/packages/runtime-core/__tests__/apiWatch.spec.ts b/packages/runtime-core/__tests__/apiWatch.spec.ts index 5e41dcd95ba..fab7d542631 100644 --- a/packages/runtime-core/__tests__/apiWatch.spec.ts +++ b/packages/runtime-core/__tests__/apiWatch.spec.ts @@ -30,7 +30,9 @@ import { shallowRef, Ref, effectScope, - toRef + toRef, + shallowReactive, + type ShallowRef } from '@vue/reactivity' // reference: https://vue-composition-api-rfc.netlify.com/api.html#watch @@ -156,7 +158,7 @@ describe('api: watch', () => { expect(dummy).toBe(1) }) - it('directly watching reactive object: deep: false', async () => { + it('directly watching reactive object with explicit deep: false', async () => { const src = reactive({ state: { count: 0 @@ -166,26 +168,47 @@ describe('api: watch', () => { watch( src, ({ state }) => { - dummy = 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) }) - it('directly watching reactive array', async () => { - const src = reactive([0]) - let dummy - watch(src, v => { - dummy = v - }) - src.push(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() - expect(dummy).toMatchObject([0, 1]) + // should trigger on array self mutation + expect(cb).toBeCalledTimes(1) }) it('watching multiple sources', async () => { diff --git a/packages/runtime-core/src/apiWatch.ts b/packages/runtime-core/src/apiWatch.ts index 8cfda87f9fb..a7532acc75b 100644 --- a/packages/runtime-core/src/apiWatch.ts +++ b/packages/runtime-core/src/apiWatch.ts @@ -209,8 +209,10 @@ function doWatch( getter = () => source.value forceTrigger = isShallow(source) } else if (isReactive(source)) { - deep = isShallow(source) ? false : deep ?? true - getter = deep ? () => source : () => shallowTraverse(source) + getter = + isShallow(source) || deep === false + ? () => traverse(source, 1) + : () => traverse(source) forceTrigger = true } else if (isArray(source)) { isMultiSource = true @@ -220,7 +222,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 { @@ -439,48 +441,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) - } - } 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 shallowTraverse(value: unknown) { - if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) { - return value - } - if (isArray(value)) { - for (let i = 0; i < value.length; i++) { - value[i] + traverse(value[i], depth, currentDepth, seen) } } else if (isSet(value) || isMap(value)) { value.forEach((v: any) => { - v + traverse(v, depth, currentDepth, seen) }) } else if (isPlainObject(value)) { for (const key in value) { - value[key] + traverse(value[key], depth, currentDepth, seen) } } return value