fix(watch): ensure watchers respect detached scope

fix #4158
This commit is contained in:
Evan You 2021-07-20 14:32:17 -04:00
parent 2bdee50a59
commit bc7f9767f5
3 changed files with 46 additions and 15 deletions

View File

@ -25,7 +25,8 @@ import {
TriggerOpTypes, TriggerOpTypes,
triggerRef, triggerRef,
shallowRef, shallowRef,
Ref Ref,
effectScope
} from '@vue/reactivity' } from '@vue/reactivity'
import { watchPostEffect } from '../src/apiWatch' import { watchPostEffect } from '../src/apiWatch'
@ -848,7 +849,7 @@ describe('api: watch', () => {
}) })
// https://github.com/vuejs/vue-next/issues/2381 // https://github.com/vuejs/vue-next/issues/2381
test('$watch should always register its effects with itw own instance', async () => { test('$watch should always register its effects with its own instance', async () => {
let instance: ComponentInternalInstance | null let instance: ComponentInternalInstance | null
let _show: Ref<boolean> let _show: Ref<boolean>
@ -889,14 +890,14 @@ describe('api: watch', () => {
expect(instance!).toBeDefined() expect(instance!).toBeDefined()
expect(instance!.scope.effects).toBeInstanceOf(Array) expect(instance!.scope.effects).toBeInstanceOf(Array)
// includes the component's own render effect AND the watcher effect // includes the component's own render effect AND the watcher effect
expect(instance!.scope.effects!.length).toBe(2) expect(instance!.scope.effects.length).toBe(2)
_show!.value = false _show!.value = false
await nextTick() await nextTick()
await nextTick() await nextTick()
expect(instance!.scope.effects![0].active).toBe(false) expect(instance!.scope.effects[0].active).toBe(false)
}) })
test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => { test('this.$watch should pass `this.proxy` to watch source as the first argument ', () => {
@ -1024,4 +1025,26 @@ describe('api: watch', () => {
expect(plus.value).toBe(true) expect(plus.value).toBe(true)
expect(count).toBe(0) expect(count).toBe(0)
}) })
// #4158
test('watch should not register in owner component if created inside detached scope', () => {
let instance: ComponentInternalInstance
const Comp = {
setup() {
instance = getCurrentInstance()!
effectScope(true).run(() => {
watch(
() => 1,
() => {}
)
})
return () => ''
}
}
const root = nodeOps.createElement('div')
createApp(Comp).mount(root)
// should not record watcher in detached scope and only the instance's
// own update effect
expect(instance!.scope.effects.length).toBe(1)
})
}) })

View File

@ -25,7 +25,9 @@ import {
import { import {
currentInstance, currentInstance,
ComponentInternalInstance, ComponentInternalInstance,
isInSSRComponentSetup isInSSRComponentSetup,
setCurrentInstance,
unsetCurrentInstance
} from './component' } from './component'
import { import {
ErrorCodes, ErrorCodes,
@ -157,8 +159,7 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
function doWatch( function doWatch(
source: WatchSource | WatchSource[] | WatchEffect | object, source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null, cb: WatchCallback | null,
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
instance = currentInstance
): WatchStopHandle { ): WatchStopHandle {
if (__DEV__ && !cb) { if (__DEV__ && !cb) {
if (immediate !== undefined) { if (immediate !== undefined) {
@ -184,6 +185,7 @@ function doWatch(
) )
} }
const instance = currentInstance
let getter: () => any let getter: () => any
let forceTrigger = false let forceTrigger = false
let isMultiSource = false let isMultiSource = false
@ -340,8 +342,7 @@ function doWatch(
} }
} }
const scope = instance && instance.scope const effect = new ReactiveEffect(getter, scheduler)
const effect = new ReactiveEffect(getter, scheduler, scope)
if (__DEV__) { if (__DEV__) {
effect.onTrack = onTrack effect.onTrack = onTrack
@ -366,8 +367,8 @@ function doWatch(
return () => { return () => {
effect.stop() effect.stop()
if (scope) { if (instance && instance.scope) {
remove(scope.effects!, effect) remove(instance.scope.effects!, effect)
} }
} }
} }
@ -392,7 +393,15 @@ export function instanceWatch(
cb = value.handler as Function cb = value.handler as Function
options = value options = value
} }
return doWatch(getter, cb.bind(publicThis), options, this) const cur = currentInstance
setCurrentInstance(this)
const res = doWatch(getter, cb.bind(publicThis), options)
if (cur) {
setCurrentInstance(cur)
} else {
unsetCurrentInstance()
}
return res
} }
export function createPathGetter(ctx: any, path: string) { export function createPathGetter(ctx: any, path: string) {

View File

@ -2304,9 +2304,8 @@ function baseCreateRenderer(
instance.emit('hook:beforeDestroy') instance.emit('hook:beforeDestroy')
} }
if (scope) { // stop effects in component scope
scope.stop() scope.stop()
}
// update may be null if a component is unmounted before its async // update may be null if a component is unmounted before its async
// setup has resolved. // setup has resolved.