]> git.ipfire.org Git - thirdparty/vuejs/core.git/commitdiff
feat(core): allow passing explicit refs via props
authorEvan You <yyx990803@gmail.com>
Wed, 6 Nov 2019 17:51:06 +0000 (12:51 -0500)
committerEvan You <yyx990803@gmail.com>
Wed, 6 Nov 2019 17:51:26 +0000 (12:51 -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/componentProps.ts
packages/runtime-core/src/componentRenderUtils.ts

index 989d4759a41da3c596a82aeda1c623f8a0cb31d3..a4364ba968a74011c4a5969a41b202ca2d8e070d 100644 (file)
@@ -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()
+    })
+  })
 })
index 4f48644513ea8fb3028420e0ffef3d70196eb203..64475e3e5a89ac9cc6e818d1948ee692e070b9db 100644 (file)
@@ -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)
+}
index 5525e2274fdeb6a2501373702c9d03004c38185e..a4b9964f9a20726ebe607b61b68a05505d1d1303 100644 (file)
@@ -4,6 +4,7 @@ export {
   isReactive,
   readonly,
   isReadonly,
+  readonlyProps,
   toRaw,
   markReadonly,
   markNonReactive
index 7b168a3b80c0a03d2b919239bd40e86c4f2c2bf6..d97f7ad0429122868df1d328323f8d9fa111c602 100644 (file)
@@ -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>,
index e1ea37659136dbf21fcdae3eacfaff3ab408928e..20139e2b26ca174ed4259e36085947d9685d31e4 100644 (file)
@@ -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) {
index ca3b7843c9d4d5a2fec05392e96fd5353bf1e4f0..41ce30963b3222703f64bcf85a8bf09c5c4ce35b 100644 (file)
@@ -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()
index 0bb1327ee46f85b4b03029ed2af2552300797313..e5964d0db179dc7991a6101087b6d5a919ed5960 100644 (file)
@@ -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 */)
       )
     }