Repository: vuejs/composition-api (ts)

Issue:
Importing a computed function triggers it
If I declare a computed property within my component and return it in the setup function to the template the computed function is not triggered.

But if I import it from a file (here it is finder.js) then the computed property is called without being called anywhere in the template.

https://codesandbox.io/s/pensive-pond-34hl9?file=/src/components/TestCase.vue

In the sandbox console you'll see a "called" from the computed function which is defined in "finder.js" despite it not being called.

While writing this I checked if a computed property that is not within an object is being called and that is not the case.

Not sure what is going on and if this is expected but it doesn't seem to be normal vue3 behavior:
https://sfc.vuejs.org/#eyJBcHAudnVlIjoiPHRlbXBsYXRlPlxuICA8aDE+e3sgbXNnIH19PC9oMT5cbjwvdGVtcGxhdGU+XG5cbjxzY3JpcHQgc2V0dXA+XG5pbXBvcnQge3JlYWN0aXZlLCBjb21wdXRlZH0gZnJvbSAndnVlJ1xuY29uc3QgbXNnID0gJ0hlbGxvIFdvcmxkISdcbmNvbnN0IHRlc3QgPSB7dGVzdDogJ2EnLCBjOiBjb21wdXRlZCgoKSA9PiB7Y29uc29sZS5sb2coXCJjYWxsZWRcIil9KX1cblxuPC9zY3JpcHQ+In0=


Pull request:
fix(setup): should not trigger getter w/ object computed nested
fix #794 

Patch:
diff --git a/src/apis/watch.ts b/src/apis/watch.ts
index 9c165f5..83522ae 100644
--- a/src/apis/watch.ts
+++ b/src/apis/watch.ts
@@ -295,6 +295,7 @@ function createWatcher(
   }
 
   let deep = options.deep
+  let isMultiSource = false
 
   let getter: () => any
   if (isRef(source)) {
@@ -303,6 +304,7 @@ function createWatcher(
     getter = () => source
     deep = true
   } else if (isArray(source)) {
+    isMultiSource = true
     getter = () =>
       source.map((s) => {
         if (isRef(s)) {
@@ -339,6 +341,8 @@ function createWatcher(
   }
 
   const applyCb = (n: any, o: any) => {
+    if (isMultiSource && n.every((v: any, i: number) => Object.is(v, o[i])))
+      return
     // cleanup before running cb again
     runCleanup()
     return cb(n, o, registerCleanup)
diff --git a/src/mixin.ts b/src/mixin.ts
index 88e4b0c..ed9fc19 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -190,7 +190,7 @@ export function mixin(Vue: VueConstructor) {
       return true
     }
 
-    if (!isPlainObject(target) || isRaw(target)) {
+    if (!isPlainObject(target) || isRaw(target) || isRef(target)) {
       return false
     }
     return Object.keys(target).some((x) =>
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index d08b7c5..f7ddc8f 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -135,7 +135,7 @@ export function observe<T>(obj: T): T {
 /**
  * Mock __ob__ for object recursively
  */
-function mockReactivityDeep(obj: any, seen = new Set()) {
+export function mockReactivityDeep(obj: any, seen = new Set()) {
   if (seen.has(obj)) return
 
   def(obj, '__ob__', mockObserver(obj))
diff --git a/src/reactivity/set.ts b/src/reactivity/set.ts
index dfbd40f..2270c9e 100644
--- a/src/reactivity/set.ts
+++ b/src/reactivity/set.ts
@@ -1,7 +1,14 @@
 import { AnyObject } from '../types/basic'
 import { getVueConstructor } from '../runtimeContext'
-import { isArray, isPrimitive, isUndef, isValidArrayIndex } from '../utils'
-import { defineAccessControl } from './reactive'
+import {
+  isArray,
+  isPrimitive,
+  isUndef,
+  isValidArrayIndex,
+  isObject,
+  hasOwn,
+} from '../utils'
+import { defineAccessControl, mockReactivityDeep } from './reactive'
 
 /**
  * Set a property on an object. Adds the new property, triggers change
@@ -49,6 +56,11 @@ export function set<T>(target: AnyObject, key: any, val: T): T {
   // IMPORTANT: define access control before trigger watcher
   defineAccessControl(target, key, val)
 
+  // in SSR, there is no __ob__. Mock for reactivity check
+  if (isObject(target[key]) && !hasOwn(target[key], '__ob__')) {
+    mockReactivityDeep(target[key])
+  }
+
   ob.dep.notify()
   return val
 }


Test patch:
diff --git a/test/apis/watch.spec.js b/test/apis/watch.spec.js
index 0bb8aeb..e01b3c3 100644
--- a/test/apis/watch.spec.js
+++ b/test/apis/watch.spec.js
@@ -5,6 +5,7 @@ const {
   watch,
   watchEffect,
   set,
+  computed,
   nextTick,
 } = require('../../src')
 const { mockWarn } = require('../helpers')
@@ -821,4 +822,28 @@ describe('api/watch', () => {
 
     expect(cb).toHaveBeenCalled()
   })
+
+  it('watching sources: ref<[]>', async () => {
+    const foo = ref([1])
+    const cb = jest.fn()
+    watch(foo, cb)
+    foo.value = foo.value.slice()
+    await nextTick()
+    expect(cb).toBeCalledTimes(1)
+  })
+
+  it('watching multiple sources: computed', async () => {
+    const number = ref(1)
+    const div2 = computed(() => {
+      return number.value > 2 ? '>2' : '<=2'
+    })
+    const div3 = computed(() => {
+      return number.value > 3 ? '>3' : '<=3'
+    })
+    const cb = jest.fn()
+    watch([div2, div3], cb)
+    number.value = 2
+    await nextTick()
+    expect(cb).toHaveBeenCalledTimes(0)
+  })
 })
diff --git a/test/setup.spec.js b/test/setup.spec.js
index ae29ec7..64f457d 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -1219,4 +1219,27 @@ describe('setup', () => {
       },
     }).$mount()
   })
+
+  // #794
+  it('should not trigger getter w/ object computed nested', () => {
+    const spy = jest.fn()
+    new Vue({
+      setup() {
+        new Vue({
+          setup() {
+            const person = {
+              name: computed(() => {
+                spy()
+                return 1
+              }),
+            }
+            return {
+              person,
+            }
+          },
+        })
+      },
+    })
+    expect(spy).toHaveBeenCalledTimes(0)
+  })
 })
diff --git a/test/setupContext.spec.ts b/test/setupContext.spec.ts
index 6ec978f..701bc95 100644
--- a/test/setupContext.spec.ts
+++ b/test/setupContext.spec.ts
@@ -3,6 +3,7 @@ import {
   defineComponent,
   createApp,
   ref,
+  computed,
   nextTick,
   SetupContext,
   getCurrentInstance,
@@ -225,4 +226,26 @@ describe('setupContext', () => {
       `"RangeError: Maximum call stack size exceeded"`
     ).not.toHaveBeenWarned()
   })
+
+  // #794
+  it('should not trigger getter w/ object computed nested', async () => {
+    const spy = jest.fn()
+    createApp(
+      defineComponent({
+        template: `<div/>`,
+        setup() {
+          const person = {
+            name: computed<number>(() => {
+              spy()
+              return 1
+            }),
+          }
+          return {
+            person,
+          }
+        },
+      })
+    ).mount()
+    expect(spy).toHaveBeenCalledTimes(0)
+  })
 })
diff --git a/test/ssr/ssrReactive.spec.ts b/test/ssr/ssrReactive.spec.ts
index 90ec277..432244b 100644
--- a/test/ssr/ssrReactive.spec.ts
+++ b/test/ssr/ssrReactive.spec.ts
@@ -145,4 +145,11 @@ describe('SSR Reactive', () => {
       `"RangeError: Maximum call stack size exceeded"`
     ).not.toHaveBeenWarned()
   })
+
+  it('should work on objects sets with set()', () => {
+    const state = ref<any>({})
+    set(state.value, 'a', {})
+
+    expect(isReactive(state.value.a)).toBe(true)
+  })
 })