Browse Source

feat(core): allow passing explicit refs via props

Evan You 6 years ago
parent
commit
d9c6ff372c

+ 30 - 1
packages/reactivity/__tests__/readonly.spec.ts

@@ -9,7 +9,8 @@ import {
   lock,
   unlock,
   effect,
-  ref
+  ref,
+  readonlyProps
 } from '../src'
 import { mockWarn } from '@vue/runtime-test'
 
@@ -442,4 +443,32 @@ describe('reactivity/readonly', () => {
       `Set operation on key "value" failed: target is readonly.`
     ).toHaveBeenWarned()
   })
+
+  describe('readonlyProps', () => {
+    test('should not unwrap root-level refs', () => {
+      const props = readonlyProps({ n: ref(1) })
+      expect(props.n.value).toBe(1)
+    })
+
+    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()
+
+      // @ts-ignore
+      props.n = 2
+      expect(props.n.value).toBe(1)
+      expect(
+        `Set operation on key "n" failed: target is readonly.`
+      ).toHaveBeenWarned()
+    })
+  })
 })

+ 14 - 5
packages/reactivity/src/baseHandlers.ts

@@ -11,16 +11,17 @@ const builtInSymbols = new Set(
     .filter(isSymbol)
 )
 
-function createGetter(isReadonly: boolean) {
+function createGetter(isReadonly: boolean, unwrap: boolean = true) {
   return function get(target: object, key: string | symbol, receiver: object) {
-    const res = Reflect.get(target, key, receiver)
+    let res = Reflect.get(target, key, receiver)
     if (isSymbol(key) && builtInSymbols.has(key)) {
       return res
     }
-    if (isRef(res)) {
-      return res.value
+    if (unwrap && isRef(res)) {
+      res = res.value
+    } else {
+      track(target, OperationTypes.GET, key)
     }
-    track(target, OperationTypes.GET, key)
     return isObject(res)
       ? isReadonly
         ? // need to lazy access readonly and reactive here to avoid
@@ -141,3 +142,11 @@ export const readonlyHandlers: ProxyHandler<object> = {
   has,
   ownKeys
 }
+
+// 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> = {
+  ...readonlyHandlers,
+  get: createGetter(true, false)
+}

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

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

+ 22 - 1
packages/reactivity/src/reactive.ts

@@ -1,5 +1,9 @@
 import { isObject, toRawType } from '@vue/shared'
-import { mutableHandlers, readonlyHandlers } from './baseHandlers'
+import {
+  mutableHandlers,
+  readonlyHandlers,
+  readonlyPropsHandlers
+} from './baseHandlers'
 import {
   mutableCollectionHandlers,
   readonlyCollectionHandlers
@@ -80,6 +84,23 @@ 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>(
+  target: T
+): Readonly<{ [K in keyof T]: UnwrapNestedRefs<T[K]> }> {
+  return createReactiveObject(
+    target,
+    rawToReadonly,
+    readonlyToRaw,
+    readonlyPropsHandlers,
+    readonlyCollectionHandlers
+  )
+}
+
 function createReactiveObject(
   target: unknown,
   toProxy: WeakMap<any, any>,

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

@@ -1,5 +1,5 @@
 import { VNode, VNodeChild, isVNode } from './vnode'
-import { ReactiveEffect, reactive, readonly } from '@vue/reactivity'
+import { ReactiveEffect, reactive, readonlyProps } 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 = readonly(instance.props))
+  const propsProxy = (instance.propsProxy = readonlyProps(instance.props))
   // 3. call setup()
   const { setup } = Component
   if (setup) {

+ 3 - 7
packages/runtime-core/src/componentProps.ts

@@ -1,4 +1,4 @@
-import { readonly, toRaw, lock, unlock } from '@vue/reactivity'
+import { toRaw, lock, unlock } from '@vue/reactivity'
 import {
   EMPTY_OBJ,
   camelize,
@@ -200,12 +200,8 @@ export function resolveProps(
   // lock readonly
   lock()
 
-  instance.props = __DEV__ ? readonly(props) : props
-  instance.attrs = options
-    ? __DEV__ && attrs != null
-      ? readonly(attrs)
-      : attrs || EMPTY_OBJ
-    : instance.props
+  instance.props = props
+  instance.attrs = options ? attrs || EMPTY_OBJ : props
 }
 
 const normalizationMap = new WeakMap()

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

@@ -14,6 +14,7 @@ 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
@@ -52,14 +53,15 @@ export function renderComponentRoot(
     } else {
       // functional
       const render = Component as FunctionalComponent
+      const propsToPass = __DEV__ ? readonlyProps(props) : props
       result = normalizeVNode(
         render.length > 1
-          ? render(props, {
+          ? render(propsToPass, {
               attrs,
               slots,
               emit
             })
-          : render(props, null as any /* we know it doesn't need it */)
+          : render(propsToPass, null as any /* we know it doesn't need it */)
       )
     }