Repository: vuejs/composition-api (ts)

Issue:
Watch always triggers when watching multiple refs
[Vue 3 sandbox](https://codesandbox.io/s/busy-wildflower-7w96c?file=/src/App.vue:11-191) | [Vue 2 sandbox](https://codesandbox.io/s/naughty-chebyshev-1yztl?file=/src/App.vue)

When watching multiple sources, the watcher fires even if the sources have not changed.

```js
const number = ref(1);

const div2 = computed(() => {
  return number.value > 2 ? ">2" : "<=2";
});

const div3 = computed(() => {
  return number.value > 3 ? ">3" : "<=3";
});

watch([div2, div3], () => {
  console.log("watcher!!");
});
```

This is not the same behaviour in vue 3. In vue 3 the watcher is only trigger if `div2` or `div3` values are changed

Pull request description:
fix(watch): always triggers when watching multiple refs
fix #790 

Patch:
diff --git a/README.md b/README.md
index 4b30be8..ea668e2 100644
--- a/README.md
+++ b/README.md
@@ -181,33 +181,6 @@ a.list[1].count === 1 // true
 
 </details>
 
-<details>
-<summary>
-⚠️ <code>set</code> and <code>del</code> workaround for adding and deleting reactive properties
-</summary>
-
-> ⚠️ Warning: `set` and `del` do NOT exist in Vue 3. We provide them as a workaround here, due to the limitation of [Vue 2.x reactivity system](https://vuejs.org/v2/guide/reactivity.html#For-Objects).
->
-> In Vue 2, you will need to call `set` to track new keys on an `object`(similar to `Vue.set` but for `reactive objects` created by the Composition API). In Vue 3, you can just assign them like normal objects.
->
-> Similarly, in Vue 2 you will need to call `del` to [ensure a key deletion triggers view updates](https://vuejs.org/v2/api/#Vue-delete) in reactive objects (similar to `Vue.delete` but for `reactive objects` created by the Composition API). In Vue 3 you can just delete them by calling `delete foo.bar`.
-
-```ts
-import { reactive, set } from '@vue/composition-api'
-
-const a = reactive({
-  foo: 1
-})
-
-// add new reactive key
-set(a, 'bar', 1)
-
-// remove a key and trigger reactivity
-del(a, 'bar')
-```
-
-</details>
-
 ### Template Refs
 
 <details>
@@ -362,6 +335,33 @@ export default {
 
 </details>
 
+<details>
+<summary>
+⚠️ <code>set</code> and <code>del</code> workaround for adding and deleting reactive properties
+</summary>
+
+> ⚠️ Warning: `set` and `del` do NOT exist in Vue 3. We provide them as a workaround here, due to the limitation of [Vue 2.x reactivity system](https://vuejs.org/v2/guide/reactivity.html#For-Objects).
+>
+> In Vue 2, you will need to call `set` to track new keys on an `object`(similar to `Vue.set` but for `reactive objects` created by the Composition API). In Vue 3, you can just assign them like normal objects.
+>
+> Similarly, in Vue 2 you will need to call `del` to [ensure a key deletion triggers view updates](https://vuejs.org/v2/api/#Vue-delete) in reactive objects (similar to `Vue.delete` but for `reactive objects` created by the Composition API). In Vue 3 you can just delete them by calling `delete foo.bar`.
+
+```ts
+import { reactive, set } from '@vue/composition-api'
+
+const a = reactive({
+  foo: 1
+})
+
+// add new reactive key
+set(a, 'bar', 1)
+
+// remove a key and trigger reactivity
+del(a, 'bar')
+```
+
+</details>
+
 ### Watch
 
 <details>
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/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/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)
+  })
 })