Skip to content

Commit bc7f976

Browse files
committed
fix(watch): ensure watchers respect detached scope
fix #4158
1 parent 2bdee50 commit bc7f976

File tree

3 files changed

+46
-15
lines changed

3 files changed

+46
-15
lines changed

packages/runtime-core/__tests__/apiWatch.spec.ts

+27-4
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ import {
2525
TriggerOpTypes,
2626
triggerRef,
2727
shallowRef,
28-
Ref
28+
Ref,
29+
effectScope
2930
} from '@vue/reactivity'
3031
import { watchPostEffect } from '../src/apiWatch'
3132

@@ -848,7 +849,7 @@ describe('api: watch', () => {
848849
})
849850

850851
// https://github.com/vuejs/vue-next/issues/2381
851-
test('$watch should always register its effects with itw own instance', async () => {
852+
test('$watch should always register its effects with its own instance', async () => {
852853
let instance: ComponentInternalInstance | null
853854
let _show: Ref<boolean>
854855

@@ -889,14 +890,14 @@ describe('api: watch', () => {
889890
expect(instance!).toBeDefined()
890891
expect(instance!.scope.effects).toBeInstanceOf(Array)
891892
// includes the component's own render effect AND the watcher effect
892-
expect(instance!.scope.effects!.length).toBe(2)
893+
expect(instance!.scope.effects.length).toBe(2)
893894

894895
_show!.value = false
895896

896897
await nextTick()
897898
await nextTick()
898899

899-
expect(instance!.scope.effects![0].active).toBe(false)
900+
expect(instance!.scope.effects[0].active).toBe(false)
900901
})
901902

902903
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@@ -1024,4 +1025,26 @@ describe('api: watch', () => {
10241025
expect(plus.value).toBe(true)
10251026
expect(count).toBe(0)
10261027
})
1028+
1029+
// #4158
1030+
test('watch should not register in owner component if created inside detached scope', () => {
1031+
let instance: ComponentInternalInstance
1032+
const Comp = {
1033+
setup() {
1034+
instance = getCurrentInstance()!
1035+
effectScope(true).run(() => {
1036+
watch(
1037+
() => 1,
1038+
() => {}
1039+
)
1040+
})
1041+
return () => ''
1042+
}
1043+
}
1044+
const root = nodeOps.createElement('div')
1045+
createApp(Comp).mount(root)
1046+
// should not record watcher in detached scope and only the instance's
1047+
// own update effect
1048+
expect(instance!.scope.effects.length).toBe(1)
1049+
})
10271050
})

packages/runtime-core/src/apiWatch.ts

+17-8
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ import {
2525
import {
2626
currentInstance,
2727
ComponentInternalInstance,
28-
isInSSRComponentSetup
28+
isInSSRComponentSetup,
29+
setCurrentInstance,
30+
unsetCurrentInstance
2931
} from './component'
3032
import {
3133
ErrorCodes,
@@ -157,8 +159,7 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
157159
function doWatch(
158160
source: WatchSource | WatchSource[] | WatchEffect | object,
159161
cb: WatchCallback | null,
160-
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
161-
instance = currentInstance
162+
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
162163
): WatchStopHandle {
163164
if (__DEV__ && !cb) {
164165
if (immediate !== undefined) {
@@ -184,6 +185,7 @@ function doWatch(
184185
)
185186
}
186187

188+
const instance = currentInstance
187189
let getter: () => any
188190
let forceTrigger = false
189191
let isMultiSource = false
@@ -340,8 +342,7 @@ function doWatch(
340342
}
341343
}
342344

343-
const scope = instance && instance.scope
344-
const effect = new ReactiveEffect(getter, scheduler, scope)
345+
const effect = new ReactiveEffect(getter, scheduler)
345346

346347
if (__DEV__) {
347348
effect.onTrack = onTrack
@@ -366,8 +367,8 @@ function doWatch(
366367

367368
return () => {
368369
effect.stop()
369-
if (scope) {
370-
remove(scope.effects!, effect)
370+
if (instance && instance.scope) {
371+
remove(instance.scope.effects!, effect)
371372
}
372373
}
373374
}
@@ -392,7 +393,15 @@ export function instanceWatch(
392393
cb = value.handler as Function
393394
options = value
394395
}
395-
return doWatch(getter, cb.bind(publicThis), options, this)
396+
const cur = currentInstance
397+
setCurrentInstance(this)
398+
const res = doWatch(getter, cb.bind(publicThis), options)
399+
if (cur) {
400+
setCurrentInstance(cur)
401+
} else {
402+
unsetCurrentInstance()
403+
}
404+
return res
396405
}
397406

398407
export function createPathGetter(ctx: any, path: string) {

packages/runtime-core/src/renderer.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -2304,9 +2304,8 @@ function baseCreateRenderer(
23042304
instance.emit('hook:beforeDestroy')
23052305
}
23062306

2307-
if (scope) {
2308-
scope.stop()
2309-
}
2307+
// stop effects in component scope
2308+
scope.stop()
23102309

23112310
// update may be null if a component is unmounted before its async
23122311
// setup has resolved.

0 commit comments

Comments
 (0)