Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(runtime-core): support deep: false when watch reactive #9928

Merged
merged 4 commits into from
Dec 30, 2023
Merged
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
55 changes: 55 additions & 0 deletions packages/runtime-core/__tests__/apiWatch.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,11 @@ import {
type DebuggerEvent,
ITERATE_KEY,
type Ref,
type ShallowRef,
TrackOpTypes,
TriggerOpTypes,
effectScope,
shallowReactive,
shallowRef,
toRef,
triggerRef,
Expand Down Expand Up @@ -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<string> = 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)
Expand Down
32 changes: 24 additions & 8 deletions packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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 {
Expand Down Expand Up @@ -460,28 +463,41 @@ export function createPathGetter(ctx: any, path: string) {
}
}

export function traverse(value: unknown, seen?: Set<unknown>) {
export function traverse(
value: unknown,
depth?: number,
currentDepth = 0,
seen?: Set<unknown>,
) {
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
Expand Down
Loading