]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
fix(core): propsProxy should not convert non-reactive nested values
authorEvan You <yyx990803@gmail.com>
Mon, 2 Dec 2019 19:11:12 +0000 (14:11 -0500)
committerEvan You <yyx990803@gmail.com>
Mon, 2 Dec 2019 19:11:12 +0000 (14:11 -0500)
packages/reactivity/__tests__/readonly.spec.ts
packages/reactivity/src/baseHandlers.ts
packages/reactivity/src/index.ts
packages/reactivity/src/reactive.ts
packages/runtime-core/src/component.ts
packages/runtime-core/src/componentRenderUtils.ts

index a4364ba968a74011c4a5969a41b202ca2d8e070d..57d286d043ce2a6e09c85be3f3c3c4c966dfb1cb 100644 (file)
@@ -10,7 +10,7 @@ import {
   unlock,
   effect,
   ref,
-  readonlyProps
+  shallowReadonly
 } from '../src'
 import { mockWarn } from '@vue/runtime-test'
 
@@ -444,31 +444,31 @@ describe('reactivity/readonly', () => {
     ).toHaveBeenWarned()
   })
 
-  describe('readonlyProps', () => {
-    test('should not unwrap root-level refs', () => {
-      const props = readonlyProps({ n: ref(1) })
-      expect(props.n.value).toBe(1)
+  describe('shallowReadonly', () => {
+    test('should not make non-reactive properties reactive', () => {
+      const props = shallowReadonly({ n: { foo: 1 } })
+      expect(isReactive(props.n)).toBe(false)
     })
 
-    test('should unwrap nested refs', () => {
-      const props = readonlyProps({ foo: { bar: ref(1) } })
-      expect(props.foo.bar).toBe(1)
-    })
-
-    test('should make properties readonly', () => {
-      const props = readonlyProps({ n: ref(1) })
-      props.n.value = 2
-      expect(props.n.value).toBe(1)
-      expect(
-        `Set operation on key "value" failed: target is readonly.`
-      ).toHaveBeenWarned()
-
+    test('should make root level properties readonly', () => {
+      const props = shallowReadonly({ n: 1 })
       // @ts-ignore
       props.n = 2
-      expect(props.n.value).toBe(1)
+      expect(props.n).toBe(1)
       expect(
         `Set operation on key "n" failed: target is readonly.`
       ).toHaveBeenWarned()
     })
+
+    // to retain 2.x behavior.
+    test('should NOT make nested properties readonly', () => {
+      const props = shallowReadonly({ n: { foo: 1 } })
+      // @ts-ignore
+      props.n.foo = 2
+      expect(props.n.foo).toBe(2)
+      expect(
+        `Set operation on key "foo" failed: target is readonly.`
+      ).not.toHaveBeenWarned()
+    })
   })
 })
index 573a67a2774ae64883e71885541aa43505f268d4..baff9231dd0ff64bf6cff0be4f84f5b2cf1423f9 100644 (file)
@@ -11,17 +11,21 @@ const builtInSymbols = new Set(
     .filter(isSymbol)
 )
 
-function createGetter(isReadonly: boolean, unwrap = true) {
+function createGetter(isReadonly: boolean, shallow = false) {
   return function get(target: object, key: string | symbol, receiver: object) {
     let res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
-    if (unwrap && isRef(res)) {
-      res = res.value
-    } else {
+    if (shallow) {
       track(target, OperationTypes.GET, key)
+      // TODO strict mode that returns a shallow-readonly version of the value
+      return res
+    }
+    if (isRef(res)) {
+      return res.value
     }
+    track(target, OperationTypes.GET, key)
     return isObject(res)
       ? isReadonly
         ? // need to lazy access readonly and reactive here to avoid
@@ -146,7 +150,7 @@ export const readonlyHandlers: ProxyHandler<object> = {
 // props handlers are special in the sense that it should not unwrap top-level
 // refs (in order to allow refs to be explicitly passed down), but should
 // retain the reactivity of the normal readonly object.
-export const readonlyPropsHandlers: ProxyHandler<object> = {
+export const shallowReadonlyHandlers: ProxyHandler<object> = {
   ...readonlyHandlers,
-  get: createGetter(true, false)
+  get: createGetter(true, true)
 }
index a4b9964f9a20726ebe607b61b68a05505d1d1303..e8a6e9bcf0c77a7ff52510fe0367eeba97544db7 100644 (file)
@@ -4,7 +4,7 @@ export {
   isReactive,
   readonly,
   isReadonly,
-  readonlyProps,
+  shallowReadonly,
   toRaw,
   markReadonly,
   markNonReactive
index d97f7ad0429122868df1d328323f8d9fa111c602..f667c24db772c75895499ce58ec3ac3db110a7de 100644 (file)
@@ -2,7 +2,7 @@ import { isObject, toRawType } from '@vue/shared'
 import {
   mutableHandlers,
   readonlyHandlers,
-  readonlyPropsHandlers
+  shallowReadonlyHandlers
 } from './baseHandlers'
 import {
   mutableCollectionHandlers,
@@ -85,18 +85,17 @@ export function readonly<T extends object>(
 }
 
 // @internal
-// Return a readonly-copy of a props object, without unwrapping refs at the root
-// level. This is intended to allow explicitly passing refs as props.
-// Technically this should use different global cache from readonly(), but
-// since it is only used on internal objects so it's not really necessary.
-export function readonlyProps<T extends object>(
+// Return a reactive-copy of the original object, where only the root level
+// properties are readonly, and does not recursively convert returned properties.
+// This is used for creating the props proxy object for stateful components.
+export function shallowReadonly<T extends object>(
   target: T
 ): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
   return createReactiveObject(
     target,
     rawToReadonly,
     readonlyToRaw,
-    readonlyPropsHandlers,
+    shallowReadonlyHandlers,
     readonlyCollectionHandlers
   )
 }
index a2d668caa38b1659c113b123a454dfcd9db46b84..45c05038e6b47dc361a6c2870c1c1e38d25b6bdf 100644 (file)
@@ -1,5 +1,5 @@
 import { VNode, VNodeChild, isVNode } from './vnode'
-import { ReactiveEffect, reactive, readonlyProps } from '@vue/reactivity'
+import { ReactiveEffect, reactive, shallowReadonly } from '@vue/reactivity'
 import {
   PublicInstanceProxyHandlers,
   ComponentPublicInstance
@@ -269,7 +269,7 @@ export function setupStatefulComponent(
   // 2. create props proxy
   // the propsProxy is a reactive AND readonly proxy to the actual props.
   // it will be updated in resolveProps() on updates before render
-  const propsProxy = (instance.propsProxy = readonlyProps(instance.props))
+  const propsProxy = (instance.propsProxy = shallowReadonly(instance.props))
   // 3. call setup()
   const { setup } = Component
   if (setup) {
index dbf68c879e6278af818801e5be28397ca4bf51dd..3b54dd437690e2df4873ce4c919142c6f8213301 100644 (file)
@@ -14,7 +14,6 @@ import { ShapeFlags } from './shapeFlags'
 import { handleError, ErrorCodes } from './errorHandling'
 import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
 import { warn } from './warning'
-import { readonlyProps } from '@vue/reactivity'
 
 // mark the current rendering instance for asset resolution (e.g.
 // resolveComponent, resolveDirective) during render
@@ -53,15 +52,14 @@ export function renderComponentRoot(
     } else {
       // functional
       const render = Component as FunctionalComponent
-      const propsToPass = __DEV__ ? readonlyProps(props) : props
       result = normalizeVNode(
         render.length > 1
-          ? render(propsToPass, {
+          ? render(props, {
               attrs,
               slots,
               emit
             })
-          : render(propsToPass, null as any /* we know it doesn't need it */)
+          : render(props, null as any /* we know it doesn't need it */)
       )
     }