Просмотр исходного кода

fix(core): propsProxy should not convert non-reactive nested values

Evan You 6 лет назад
Родитель
Сommit
57bbbb227c

+ 19 - 19
packages/reactivity/__tests__/readonly.spec.ts

@@ -10,7 +10,7 @@ import {
   unlock,
   unlock,
   effect,
   effect,
   ref,
   ref,
-  readonlyProps
+  shallowReadonly
 } from '../src'
 } from '../src'
 import { mockWarn } from '@vue/runtime-test'
 import { mockWarn } from '@vue/runtime-test'
 
 
@@ -444,31 +444,31 @@ describe('reactivity/readonly', () => {
     ).toHaveBeenWarned()
     ).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
       // @ts-ignore
       props.n = 2
       props.n = 2
-      expect(props.n.value).toBe(1)
+      expect(props.n).toBe(1)
       expect(
       expect(
         `Set operation on key "n" failed: target is readonly.`
         `Set operation on key "n" failed: target is readonly.`
       ).toHaveBeenWarned()
       ).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()
+    })
   })
   })
 })
 })

+ 10 - 6
packages/reactivity/src/baseHandlers.ts

@@ -11,17 +11,21 @@ const builtInSymbols = new Set(
     .filter(isSymbol)
     .filter(isSymbol)
 )
 )
 
 
-function createGetter(isReadonly: boolean, unwrap = true) {
+function createGetter(isReadonly: boolean, shallow = false) {
   return function get(target: object, key: string | symbol, receiver: object) {
   return function get(target: object, key: string | symbol, receiver: object) {
     let res = Reflect.get(target, key, receiver)
     let res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
       return res
     }
     }
-    if (unwrap && isRef(res)) {
-      res = res.value
-    } else {
+    if (shallow) {
       track(target, OperationTypes.GET, key)
       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)
     return isObject(res)
       ? isReadonly
       ? isReadonly
         ? // need to lazy access readonly and reactive here to avoid
         ? // 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
 // 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
 // refs (in order to allow refs to be explicitly passed down), but should
 // retain the reactivity of the normal readonly object.
 // retain the reactivity of the normal readonly object.
-export const readonlyPropsHandlers: ProxyHandler<object> = {
+export const shallowReadonlyHandlers: ProxyHandler<object> = {
   ...readonlyHandlers,
   ...readonlyHandlers,
-  get: createGetter(true, false)
+  get: createGetter(true, true)
 }
 }

+ 1 - 1
packages/reactivity/src/index.ts

@@ -4,7 +4,7 @@ export {
   isReactive,
   isReactive,
   readonly,
   readonly,
   isReadonly,
   isReadonly,
-  readonlyProps,
+  shallowReadonly,
   toRaw,
   toRaw,
   markReadonly,
   markReadonly,
   markNonReactive
   markNonReactive

+ 6 - 7
packages/reactivity/src/reactive.ts

@@ -2,7 +2,7 @@ import { isObject, toRawType } from '@vue/shared'
 import {
 import {
   mutableHandlers,
   mutableHandlers,
   readonlyHandlers,
   readonlyHandlers,
-  readonlyPropsHandlers
+  shallowReadonlyHandlers
 } from './baseHandlers'
 } from './baseHandlers'
 import {
 import {
   mutableCollectionHandlers,
   mutableCollectionHandlers,
@@ -85,18 +85,17 @@ export function readonly<T extends object>(
 }
 }
 
 
 // @internal
 // @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
   target: T
 ): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
 ): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
   return createReactiveObject(
   return createReactiveObject(
     target,
     target,
     rawToReadonly,
     rawToReadonly,
     readonlyToRaw,
     readonlyToRaw,
-    readonlyPropsHandlers,
+    shallowReadonlyHandlers,
     readonlyCollectionHandlers
     readonlyCollectionHandlers
   )
   )
 }
 }

+ 2 - 2
packages/runtime-core/src/component.ts

@@ -1,5 +1,5 @@
 import { VNode, VNodeChild, isVNode } from './vnode'
 import { VNode, VNodeChild, isVNode } from './vnode'
-import { ReactiveEffect, reactive, readonlyProps } from '@vue/reactivity'
+import { ReactiveEffect, reactive, shallowReadonly } from '@vue/reactivity'
 import {
 import {
   PublicInstanceProxyHandlers,
   PublicInstanceProxyHandlers,
   ComponentPublicInstance
   ComponentPublicInstance
@@ -269,7 +269,7 @@ export function setupStatefulComponent(
   // 2. create props proxy
   // 2. create props proxy
   // the propsProxy is a reactive AND readonly proxy to the actual props.
   // the propsProxy is a reactive AND readonly proxy to the actual props.
   // it will be updated in resolveProps() on updates before render
   // 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()
   // 3. call setup()
   const { setup } = Component
   const { setup } = Component
   if (setup) {
   if (setup) {

+ 2 - 4
packages/runtime-core/src/componentRenderUtils.ts

@@ -14,7 +14,6 @@ import { ShapeFlags } from './shapeFlags'
 import { handleError, ErrorCodes } from './errorHandling'
 import { handleError, ErrorCodes } from './errorHandling'
 import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
 import { PatchFlags, EMPTY_OBJ } from '@vue/shared'
 import { warn } from './warning'
 import { warn } from './warning'
-import { readonlyProps } from '@vue/reactivity'
 
 
 // mark the current rendering instance for asset resolution (e.g.
 // mark the current rendering instance for asset resolution (e.g.
 // resolveComponent, resolveDirective) during render
 // resolveComponent, resolveDirective) during render
@@ -53,15 +52,14 @@ export function renderComponentRoot(
     } else {
     } else {
       // functional
       // functional
       const render = Component as FunctionalComponent
       const render = Component as FunctionalComponent
-      const propsToPass = __DEV__ ? readonlyProps(props) : props
       result = normalizeVNode(
       result = normalizeVNode(
         render.length > 1
         render.length > 1
-          ? render(propsToPass, {
+          ? render(props, {
               attrs,
               attrs,
               slots,
               slots,
               emit
               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 */)
       )
       )
     }
     }