]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(watch): ensure watchers respect detached scope
authorEvan You <yyx990803@gmail.com>
Tue, 20 Jul 2021 18:32:17 +0000 (14:32 -0400)
committerEvan You <yyx990803@gmail.com>
Tue, 20 Jul 2021 18:32:17 +0000 (14:32 -0400)
fix #4158

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

index 1fa946c3e05a24dcba51c233895c911a6322c050..6ac335eaa58a265fa4dd4bf927ea76861101f4c8 100644 (file)
@@ -25,7 +25,8 @@ import {
   TriggerOpTypes,
   triggerRef,
   shallowRef,
-  Ref
+  Ref,
+  effectScope
 } from '@vue/reactivity'
 import { watchPostEffect } from '../src/apiWatch'
 
@@ -848,7 +849,7 @@ describe('api: watch', () => {
   })
 
   // 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 _show: Ref<boolean>
 
@@ -889,14 +890,14 @@ describe('api: watch', () => {
     expect(instance!).toBeDefined()
     expect(instance!.scope.effects).toBeInstanceOf(Array)
     // 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
 
     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 ', () => {
@@ -1024,4 +1025,26 @@ describe('api: watch', () => {
     expect(plus.value).toBe(true)
     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)
+  })
 })
index 2747615b7b3d29e02942ae2043d7e41ec6e3fdf5..f89694df8048ca5ab7aeca32d7e95634b44fb0f5 100644 (file)
@@ -25,7 +25,9 @@ import {
 import {
   currentInstance,
   ComponentInternalInstance,
-  isInSSRComponentSetup
+  isInSSRComponentSetup,
+  setCurrentInstance,
+  unsetCurrentInstance
 } from './component'
 import {
   ErrorCodes,
@@ -157,8 +159,7 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
 function doWatch(
   source: WatchSource | WatchSource[] | WatchEffect | object,
   cb: WatchCallback | null,
-  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ,
-  instance = currentInstance
+  { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
 ): WatchStopHandle {
   if (__DEV__ && !cb) {
     if (immediate !== undefined) {
@@ -184,6 +185,7 @@ function doWatch(
     )
   }
 
+  const instance = currentInstance
   let getter: () => any
   let forceTrigger = false
   let isMultiSource = false
@@ -340,8 +342,7 @@ function doWatch(
     }
   }
 
-  const scope = instance && instance.scope
-  const effect = new ReactiveEffect(getter, scheduler, scope)
+  const effect = new ReactiveEffect(getter, scheduler)
 
   if (__DEV__) {
     effect.onTrack = onTrack
@@ -366,8 +367,8 @@ function doWatch(
 
   return () => {
     effect.stop()
-    if (scope) {
-      remove(scope.effects!, effect)
+    if (instance && instance.scope) {
+      remove(instance.scope.effects!, effect)
     }
   }
 }
@@ -392,7 +393,15 @@ export function instanceWatch(
     cb = value.handler as Function
     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) {
index 8ac4536f03cc8585397f2df381c7a5d809a670a4..4bf1f9038fe2f90263478d8bf64265608eef0468 100644 (file)
@@ -2304,9 +2304,8 @@ function baseCreateRenderer(
       instance.emit('hook:beforeDestroy')
     }
 
-    if (scope) {
-      scope.stop()
-    }
+    // stop effects in component scope
+    scope.stop()
 
     // update may be null if a component is unmounted before its async
     // setup has resolved.