Repository: vuejs/composition-api
Pull request #697 (2021-05-21 02:03:25)

fix(dev): setup data in nextTick is proxied to vm._data.
fix: #679 

In DEV mode, setup data in nextTick is proxied to vm._data. As a result, setup data is displayed after the component is updated. 


Patch:
diff --git a/README.md b/README.md
index c6c1f02..d5f484f 100644
--- a/README.md
+++ b/README.md
@@ -122,27 +122,6 @@ export default {
 
 ### `Ref` Unwrap
 
-`Unwrap` is not working with Array index.
-
-<details>
-<summary>
-❌ <b>Should NOT</b> store <code>ref</code> as a <b>direct</b> child of <code>Array</code>
-</summary>
-
-```js
-const state = reactive({
-  list: [ref(0)],
-})
-// no unwrap, `.value` is required
-state.list[0].value === 0 // true
-
-state.list.push(ref(1))
-// no unwrap, `.value` is required
-state.list[1].value === 1 // true
-```
-
-</details>
-
 <details>
 <summary>
 ❌ <b>Should NOT</b> use <code>ref</code> in a plain object when working with <code>Array</code>
@@ -437,7 +416,7 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
 
 <details>
 <summary>
-⚠️ <code>readonly()</code> provides **only type-level** readonly check. 
+⚠️ <code>readonly()</code> provides <b>only type-level</b> readonly check. 
 </summary>
 
 `readonly()` is provided as API alignment with Vue 3 on type-level only. Use <code>isReadonly()</code> on it or it's properties can not be guaranteed.
@@ -448,8 +427,8 @@ app2.component('Bar', Bar) // equivalent to Vue.use('Bar', Bar)
 
 <details>
 <summary>
-⚠️ <code>toRefs(props.foo.bar)</code> will incorrectly warn when acessing nested levels of props.
-⚠️ <code>isReactive(props.foo.bar)</code> will return false.
+⚠️ <code>toRefs(props.foo)</code> will incorrectly warn when accessing nested levels of props. <br>
+&nbsp;&nbsp;&nbsp;&nbsp;⚠️ <code>isReactive(props.foo)</code> will return false.
 </summary>
 
 ```ts
diff --git a/README.zh-CN.md b/README.zh-CN.md
index 913d18c..b2686a6 100644
--- a/README.zh-CN.md
+++ b/README.zh-CN.md
@@ -117,27 +117,6 @@ export default {
 
 ### `Ref` 自动展开 (unwrap)
 
-数组索引属性无法进行自动展开:
-
-<details>
-<summary>
-❌ <b>不要</b> 使用数组直接存取 <code>ref</code> 对象
-</summary>
-
-```js
-const state = reactive({
-  list: [ref(0)],
-})
-// 不会自动展开, 须使用 `.value`
-state.list[0].value === 0 // true
-
-state.list.push(ref(1))
-// 不会自动展开, 须使用 `.value`
-state.list[1].value === 1 // true
-```
-
-</details>
-
 <details>
 <summary>
 ❌ <b>不要</b> 在数组中使用含有 <code>ref</code> 的普通对象
@@ -431,8 +410,8 @@ watch(
 
 <details>
 <summary>
-⚠️ 当使用 <code>toRefs</code> 访问深层属性对象 （如 <code>toRefs(props.foo.bar)</code> 时将会得到不正确的警告。
-⚠️ <code>isReactive(props.foo.bar)</code> 将会返回 false。
+⚠️ 当使用 <code>toRefs</code> 访问深层属性对象 （如 <code>toRefs(props.foo)</code> 时将会得到不正确的警告。<br>
+&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;⚠️ <code>isReactive(props.foo)</code> 将会返回 false。
 </summary>
   
 ```ts
diff --git a/src/apis/watch.ts b/src/apis/watch.ts
index f10f9bd..3f61414 100644
--- a/src/apis/watch.ts
+++ b/src/apis/watch.ts
@@ -1,6 +1,17 @@
 import { ComponentInstance } from '../component'
 import { Ref, isRef, isReactive } from '../reactivity'
-import { assert, logError, noopFn, warn, isFunction } from '../utils'
+import {
+  assert,
+  logError,
+  noopFn,
+  warn,
+  isFunction,
+  isObject,
+  isArray,
+  isPlainObject,
+  isSet,
+  isMap,
+} from '../utils'
 import { defineComponentInstance } from '../utils/helper'
 import { getCurrentInstance, getVueConstructor } from '../runtimeContext'
 import {
@@ -272,13 +283,29 @@ function createWatcher(
   let deep = options.deep
 
   let getter: () => any
-  if (Array.isArray(source)) {
-    getter = () => source.map((s) => (isRef(s) ? s.value : s()))
-  } else if (isRef(source)) {
+  if (isRef(source)) {
     getter = () => source.value
   } else if (isReactive(source)) {
     getter = () => source
     deep = true
+  } else if (isArray(source)) {
+    getter = () =>
+      source.map((s) => {
+        if (isRef(s)) {
+          return s.value
+        } else if (isReactive(s)) {
+          return traverse(s)
+        } else if (isFunction(s)) {
+          return s()
+        } else {
+          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
+          )
+          return noopFn
+        }
+      })
   } else if (isFunction(source)) {
     getter = source as () => any
   } else {
@@ -405,3 +432,26 @@ export function watch<T = any>(
 
   return createWatcher(vm, source, callback, opts)
 }
+
+function traverse(value: unknown, seen: Set<unknown> = new Set()) {
+  if (!isObject(value) || seen.has(value)) {
+    return value
+  }
+  seen.add(value)
+  if (isRef(value)) {
+    traverse(value.value, seen)
+  } else if (isArray(value)) {
+    for (let i = 0; i < value.length; i++) {
+      traverse(value[i], seen)
+    }
+  } else if (isSet(value) || isMap(value)) {
+    value.forEach((v: any) => {
+      traverse(v, seen)
+    })
+  } else if (isPlainObject(value)) {
+    for (const key in value) {
+      traverse((value as any)[key], seen)
+    }
+  }
+  return value
+}
diff --git a/src/reactivity/readonly.ts b/src/reactivity/readonly.ts
index c4e3445..dc4bcbd 100644
--- a/src/reactivity/readonly.ts
+++ b/src/reactivity/readonly.ts
@@ -1,6 +1,7 @@
 import { reactive, Ref, UnwrapRef } from '.'
 import { isArray, isPlainObject, warn } from '../utils'
 import { readonlySet } from '../utils/sets'
+import { isRef } from './ref'
 
 export function isReadonly(obj: any): boolean {
   return readonlySet.has(obj)
@@ -47,7 +48,10 @@ export function readonly<T extends object>(
 
 export function shallowReadonly<T extends object>(obj: T): Readonly<T>
 export function shallowReadonly(obj: any): any {
-  if (!(isPlainObject(obj) || isArray(obj)) || !Object.isExtensible(obj)) {
+  if (
+    !(isPlainObject(obj) || isArray(obj)) ||
+    (!Object.isExtensible(obj) && !isRef(obj))
+  ) {
     return obj
   }
 
@@ -60,7 +64,7 @@ export function shallowReadonly(obj: any): any {
     let val = obj[key]
     let getter: (() => any) | undefined
     const property = Object.getOwnPropertyDescriptor(obj, key)
-    if (property) {
+    if (property && !isRef(obj)) {
       if (property.configurable === false) {
         continue
       }
diff --git a/src/utils/instance.ts b/src/utils/instance.ts
index 5abfa75..157a108 100644
--- a/src/utils/instance.ts
+++ b/src/utils/instance.ts
@@ -39,6 +39,9 @@ export function asVmProperty(
       // expose binding to Vue Devtool as a data property
       // delay this until state has been resolved to prevent repeated works
       vm.$nextTick(() => {
+        if (Object.keys(vm._data).indexOf(propName) !== -1) {
+          return
+        }
         if (isRef(propValue)) {
           proxy(vm._data, propName, {
             get: () => propValue.value,
diff --git a/src/utils/utils.ts b/src/utils/utils.ts
index dff6823..9f2e79e 100644
--- a/src/utils/utils.ts
+++ b/src/utils/utils.ts
@@ -60,6 +60,17 @@ export function isArray<T>(x: unknown): x is T[] {
   return Array.isArray(x)
 }
 
+export const objectToString = Object.prototype.toString
+
+export const toTypeString = (value: unknown): string =>
+  objectToString.call(value)
+
+export const isMap = (val: unknown): val is Map<any, any> =>
+  toTypeString(val) === '[object Map]'
+
+export const isSet = (val: unknown): val is Set<any> =>
+  toTypeString(val) === '[object Set]'
+
 export function isValidArrayIndex(val: any): boolean {
   const n = parseFloat(String(val))
   return n >= 0 && Math.floor(n) === n && isFinite(val)


Test patch:
diff --git a/test/setup.spec.js b/test/setup.spec.js
index e1ac9a6..d63f72c 100644
--- a/test/setup.spec.js
+++ b/test/setup.spec.js
@@ -942,6 +942,34 @@ describe('setup', () => {
     expect(vm.$el.textContent).toBe('1')
   })
 
+  // #679 html text change
+  it('should id not change when msg changed in development', async () => {
+    global.__DEV__ = true
+    const vm = new Vue({
+      template: '<div>{{ id }} {{ msg }}<button @click="change"/></div>',
+      setup() {
+        return { id: 42 }
+      },
+      data() {
+        return {
+          id: 1,
+          msg: 'abc',
+        }
+      },
+      methods: {
+        change() {
+          this.msg = this.msg + this.id
+        },
+      },
+    }).$mount()
+
+    await nextTick()
+    expect(vm.$el.textContent).toBe('1 abc')
+    await vm.$el.querySelector('button').click()
+    await nextTick()
+    expect(vm.$el.textContent).toBe('1 abc1')
+  })
+
   // #683 #603 #580
   it('should update directly when adding attributes to a reactive object', async () => {
     const vm = new Vue({
diff --git a/test/v3/reactivity/readonly.spec.ts b/test/v3/reactivity/readonly.spec.ts
index b6cc56d..5513130 100644
--- a/test/v3/reactivity/readonly.spec.ts
+++ b/test/v3/reactivity/readonly.spec.ts
@@ -1,5 +1,7 @@
 import { mockWarn } from '../../helpers/mockWarn'
-import { shallowReadonly, isReactive } from '../../../src'
+import { shallowReadonly, isReactive, ref, reactive } from '../../../src'
+
+const Vue = require('vue/dist/vue.common.js')
 
 // /**
 //  * @see https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-4.html
@@ -372,5 +374,32 @@ describe('reactivity/readonly', () => {
         `Set operation on key "foo" failed: target is readonly.`
       ).not.toHaveBeenWarned()
     })
+
+    // #669
+    test('shallowReadonly should work for refs', () => {
+      const vm = new Vue({
+        template: '<div>{{ count }} {{ countRef }}</div>',
+        setup() {
+          const count = reactive({ number: 0 })
+          const countRef = ref(0)
+
+          const countReadonly = shallowReadonly(count)
+          const countRefReadonly = shallowReadonly(countRef)
+
+          setTimeout(() => {
+            // @ts-expect-error
+            countReadonly.number++
+            // @ts-expect-error
+            countRefReadonly.value++
+          }, 2000)
+          return {
+            count,
+            countRef,
+          }
+        },
+      }).$mount()
+
+      expect(vm.$el.textContent).toBe(`{\n  "number": 0\n} 0`)
+    })
   })
 })