Repository: vuejs/composition-api
Pull request #758 (2021-07-09 06:55:53)

fix(customReactive): avoid circular reference.
fix: #563 


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 a03ba55..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
@@ -319,6 +332,11 @@ function createWatcher(
       )
   }
 
+  if (deep) {
+    const baseGetter = getter
+    getter = () => traverse(baseGetter())
+  }
+
   const applyCb = (n: any, o: any) => {
     // cleanup before running cb again
     runCleanup()
diff --git a/src/mixin.ts b/src/mixin.ts
index 3adde30..3dc3b19 100644
--- a/src/mixin.ts
+++ b/src/mixin.ts
@@ -149,7 +149,8 @@ export function mixin(Vue: VueConstructor) {
     }
   }
 
-  function customReactive(target: object) {
+  function customReactive(target: object, visited = new Set()) {
+    if (visited.has(target)) return
     if (
       !isPlainObject(target) ||
       isRef(target) ||
@@ -165,7 +166,8 @@ export function mixin(Vue: VueConstructor) {
       const val = target[k]
       defineReactive(target, k, val)
       if (val) {
-        customReactive(val)
+        visited.add(val)
+        customReactive(val, visited)
       }
       return
     })
@@ -176,7 +178,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 +212,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 +240,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/setupContext.spec.ts b/test/setupContext.spec.ts
index cb60203..6ec978f 100644
--- a/test/setupContext.spec.ts
+++ b/test/setupContext.spec.ts
@@ -5,9 +5,12 @@ import {
   ref,
   nextTick,
   SetupContext,
+  getCurrentInstance,
 } from '../src'
+import { mockWarn } from './helpers'
 
 describe('setupContext', () => {
+  mockWarn(true)
   it('should have proper properties', () => {
     let context: SetupContext = undefined!
 
@@ -195,4 +198,31 @@ describe('setupContext', () => {
 
     expect(_attrs.foo).toBe('bar2')
   })
+
+  // #563
+  it('should not RangeError: Maximum call stack size exceeded', async () => {
+    createApp(
+      defineComponent({
+        template: `<div/>`,
+        setup() {
+          // @ts-expect-error
+          const app = getCurrentInstance().proxy
+          let mockNT: any = []
+          mockNT.__ob__ = {}
+          const test = {
+            app,
+            mockNT,
+          }
+          return {
+            test,
+          }
+        },
+      })
+    ).mount()
+
+    await nextTick()
+    expect(
+      `"RangeError: Maximum call stack size exceeded"`
+    ).not.toHaveBeenWarned()
+  })
 })
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])
+  })
 })