Repository: vuejs/composition-api
Pull request #753 (2021-07-07 10:38:46)

fix(watch): traverse refs in deep watch
fix: [#752](https://github.com/vuejs/composition-api/issues/752)


Patch:
diff --git a/src/apis/createElement.ts b/src/apis/createElement.ts
index 07ae325..6e05d9d 100644
--- a/src/apis/createElement.ts
+++ b/src/apis/createElement.ts
@@ -7,17 +7,19 @@ type CreateElement = Vue['$createElement']
 
 let fallbackCreateElement: CreateElement
 
-export const createElement = (function createElement(...args: any) {
+export const createElement = function createElement(...args: any) {
   const instance = getCurrentInstance()?.proxy
   if (!instance) {
-    warn('`createElement()` has been called outside of render function.')
+    __DEV__ &&
+      warn('`createElement()` has been called outside of render function.')
     if (!fallbackCreateElement) {
-      fallbackCreateElement = defineComponentInstance(getVueConstructor())
-        .$createElement
+      fallbackCreateElement = defineComponentInstance(
+        getVueConstructor()
+      ).$createElement
     }
 
     return fallbackCreateElement.apply(fallbackCreateElement, args)
   }
 
   return instance.$createElement.apply(instance, args)
-} as any) as CreateElement
+} as any as CreateElement
diff --git a/src/apis/inject.ts b/src/apis/inject.ts
index 8247a91..4418379 100644
--- a/src/apis/inject.ts
+++ b/src/apis/inject.ts
@@ -54,7 +54,8 @@ export function inject(
 
   const vm = getCurrentInstance()?.proxy
   if (!vm) {
-    warn(`inject() can only be used inside setup() or functional components.`)
+    __DEV__ &&
+      warn(`inject() can only be used inside setup() or functional components.`)
     return
   }
 
diff --git a/src/apis/watch.ts b/src/apis/watch.ts
index abddb89..5073a60 100644
--- a/src/apis/watch.ts
+++ b/src/apis/watch.ts
@@ -103,8 +103,6 @@ function getWatcherOption(options?: Partial<WatchOptions>): WatchOptions {
 function getWatchEffectOption(options?: Partial<WatchOptions>): WatchOptions {
   return {
     ...{
-      immediate: true,
-      deep: false,
       flush: 'pre',
     },
     ...options,
@@ -208,6 +206,21 @@ function createWatcher(
   cb: WatchCallback<any> | null,
   options: WatchOptions
 ): () => void {
+  if (__DEV__ && !cb) {
+    if (options.immediate !== undefined) {
+      warn(
+        `watch() "immediate" option is only respected when using the ` +
+          `watch(source, callback, options?) signature.`
+      )
+    }
+    if (options.deep !== undefined) {
+      warn(
+        `watch() "deep" option is only respected when using the ` +
+          `watch(source, callback, options?) signature.`
+      )
+    }
+  }
+
   const flushMode = options.flush
   const isSync = flushMode === 'sync'
   let cleanup: (() => void) | null
@@ -298,11 +311,12 @@ function createWatcher(
         } else if (isFunction(s)) {
           return s()
         } else {
-          warn(
-            `Invalid watch source: ${JSON.stringify(s)}.
+          __DEV__ &&
+            warn(
+              `Invalid watch source: ${JSON.stringify(s)}.
           A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`,
-            vm
-          )
+              vm
+            )
           return noopFn
         }
       })
@@ -310,11 +324,17 @@ function createWatcher(
     getter = source as () => any
   } else {
     getter = noopFn
-    warn(
-      `Invalid watch source: ${JSON.stringify(source)}.
+    __DEV__ &&
+      warn(
+        `Invalid watch source: ${JSON.stringify(source)}.
       A watch source can only be a getter/effect function, a ref, a reactive object, or an array of these types.`,
-      vm
-    )
+        vm
+      )
+  }
+
+  if (deep) {
+    const baseGetter = getter
+    getter = () => traverse(baseGetter())
   }
 
   const applyCb = (n: any, o: any) => {
diff --git a/src/mixin.ts b/src/mixin.ts
index 3adde30..078c846 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -176,7 +176,7 @@ export function mixin(Vue: VueConstructor) {
       return visited.get(target)
     }
     visited.set(target, false)
-    if (Array.isArray(target) && isReactive(target)) {
+    if (isArray(target) && isReactive(target)) {
       visited.set(target, true)
       return true
     }
@@ -210,10 +210,11 @@ export function mixin(Vue: VueConstructor) {
       proxy(ctx, key, {
         get: () => vm[srcKey],
         set() {
-          warn(
-            `Cannot assign to '${key}' because it is a read-only property`,
-            vm
-          )
+          __DEV__ &&
+            warn(
+              `Cannot assign to '${key}' because it is a read-only property`,
+              vm
+            )
         },
       })
     })
@@ -237,10 +238,11 @@ export function mixin(Vue: VueConstructor) {
           return data
         },
         set() {
-          warn(
-            `Cannot assign to '${key}' because it is a read-only property`,
-            vm
-          )
+          __DEV__ &&
+            warn(
+              `Cannot assign to '${key}' because it is a read-only property`,
+              vm
+            )
         },
       })
     })
diff --git a/src/reactivity/del.ts b/src/reactivity/del.ts
index aceb324..a433142 100644
--- a/src/reactivity/del.ts
+++ b/src/reactivity/del.ts
@@ -1,6 +1,12 @@
 import { AnyObject } from '../types/basic'
 import { getVueConstructor } from '../runtimeContext'
-import { hasOwn, isPrimitive, isUndef, isValidArrayIndex } from '../utils'
+import {
+  hasOwn,
+  isPrimitive,
+  isUndef,
+  isArray,
+  isValidArrayIndex,
+} from '../utils'
 
 /**
  * Delete a property and trigger change if necessary.
@@ -14,7 +20,7 @@ export function del(target: AnyObject, key: any) {
       `Cannot delete reactive property on undefined, null, or primitive value: ${target}`
     )
   }
-  if (Array.isArray(target) && isValidArrayIndex(key)) {
+  if (isArray(target) && isValidArrayIndex(key)) {
     target.splice(key, 1)
     return
   }
diff --git a/src/reactivity/reactive.ts b/src/reactivity/reactive.ts
index 90184f8..291a990 100644
--- a/src/reactivity/reactive.ts
+++ b/src/reactivity/reactive.ts
@@ -41,7 +41,7 @@ function setupAccessControl(target: AnyObject): void {
   if (
     !isPlainObject(target) ||
     isRaw(target) ||
-    Array.isArray(target) ||
+    isArray(target) ||
     isRef(target) ||
     isComponentInstance(target) ||
     accessModifiedSet.has(target)
diff --git a/src/utils/helper.ts b/src/utils/helper.ts
index b72ce79..753bc90 100644
--- a/src/utils/helper.ts
+++ b/src/utils/helper.ts
@@ -34,10 +34,12 @@ export function isComponentInstance(obj: any) {
 export function createSlotProxy(vm: ComponentInstance, slotName: string) {
   return (...args: any) => {
     if (!vm.$scopedSlots[slotName]) {
-      return warn(
-        `slots.${slotName}() got called outside of the "render()" scope`,
-        vm
-      )
+      if (__DEV__)
+        return warn(
+          `slots.${slotName}() got called outside of the "render()" scope`,
+          vm
+        )
+      return
     }
 
     return vm.$scopedSlots[slotName]!.apply(vm, args)


Test patch:
diff --git a/test/apis/watch.spec.js b/test/apis/watch.spec.js
index ab946e1..0bb8aeb 100644
--- a/test/apis/watch.spec.js
+++ b/test/apis/watch.spec.js
@@ -446,6 +446,40 @@ describe('api/watch', () => {
       vm.count++
       expect(spy).toHaveBeenLastCalledWith(1)
     })
+
+    it('warn immediate option when using effect', async () => {
+      const count = ref(0)
+      let dummy
+      watchEffect(
+        () => {
+          dummy = count.value
+        },
+        { immediate: false }
+      )
+      expect(dummy).toBe(0)
+      expect(`"immediate" option is only respected`).toHaveBeenWarned()
+
+      count.value++
+      await nextTick()
+      expect(dummy).toBe(1)
+    })
+
+    it('warn and not respect deep option when using effect', async () => {
+      const arr = ref([1, [2]])
+      const spy = jest.fn()
+      watchEffect(
+        () => {
+          spy()
+          return arr
+        },
+        { deep: true }
+      )
+      expect(spy).toHaveBeenCalledTimes(1)
+      arr.value[1][0] = 3
+      await nextTick()
+      expect(spy).toHaveBeenCalledTimes(1),
+        expect(`"deep" option is only respected`).toHaveBeenWarned()
+    })
   })
 
   describe('Multiple sources', () => {
diff --git a/test/v3/runtime-core/apiWatch.spec.ts b/test/v3/runtime-core/apiWatch.spec.ts
index b98d95b..6fde5b2 100644
--- a/test/v3/runtime-core/apiWatch.spec.ts
+++ b/test/v3/runtime-core/apiWatch.spec.ts
@@ -531,4 +531,23 @@ describe('api: watch', () => {
 
     expect(data2.value).toMatchObject([1])
   })
+
+  it('watching deep ref', async () => {
+    const count = ref(0)
+    const double = computed(() => count.value * 2)
+    const state = reactive([count, double])
+
+    let dummy
+    watch(
+      () => state,
+      (state) => {
+        dummy = [state[0].value, state[1].value]
+      },
+      { deep: true }
+    )
+
+    count.value++
+    await nextTick()
+    expect(dummy).toEqual([1, 2])
+  })
 })