From d9c6ff372c10dde8b496ee32f2b9a246edf66a35 Mon Sep 17 00:00:00 2001 From: Evan You Date: Wed, 6 Nov 2019 12:51:06 -0500 Subject: [PATCH] feat(core): allow passing explicit refs via props --- .../reactivity/__tests__/readonly.spec.ts | 31 ++++++++++++++++++- packages/reactivity/src/baseHandlers.ts | 19 +++++++++--- packages/reactivity/src/index.ts | 1 + packages/reactivity/src/reactive.ts | 23 +++++++++++++- packages/runtime-core/src/component.ts | 4 +-- packages/runtime-core/src/componentProps.ts | 10 ++---- .../runtime-core/src/componentRenderUtils.ts | 6 ++-- 7 files changed, 76 insertions(+), 18 deletions(-) diff --git a/packages/reactivity/__tests__/readonly.spec.ts b/packages/reactivity/__tests__/readonly.spec.ts index 989d4759a..a4364ba96 100644 --- a/packages/reactivity/__tests__/readonly.spec.ts +++ b/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() + }) + }) }) diff --git a/packages/reactivity/src/baseHandlers.ts b/packages/reactivity/src/baseHandlers.ts index 4f4864451..64475e3e5 100644 --- a/packages/reactivity/src/baseHandlers.ts +++ b/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 = { 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 = { + ...readonlyHandlers, + get: createGetter(true, false) +} diff --git a/packages/reactivity/src/index.ts b/packages/reactivity/src/index.ts index 5525e2274..a4b9964f9 100644 --- a/packages/reactivity/src/index.ts +++ b/packages/reactivity/src/index.ts @@ -4,6 +4,7 @@ export { isReactive, readonly, isReadonly, + readonlyProps, toRaw, markReadonly, markNonReactive diff --git a/packages/reactivity/src/reactive.ts b/packages/reactivity/src/reactive.ts index 7b168a3b8..d97f7ad04 100644 --- a/packages/reactivity/src/reactive.ts +++ b/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( ) } +// @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( + target: T +): Readonly<{ [K in keyof T]: UnwrapNestedRefs }> { + return createReactiveObject( + target, + rawToReadonly, + readonlyToRaw, + readonlyPropsHandlers, + readonlyCollectionHandlers + ) +} + function createReactiveObject( target: unknown, toProxy: WeakMap, diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index e1ea37659..20139e2b2 100644 --- a/packages/runtime-core/src/component.ts +++ b/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) { diff --git a/packages/runtime-core/src/componentProps.ts b/packages/runtime-core/src/componentProps.ts index ca3b7843c..41ce30963 100644 --- a/packages/runtime-core/src/componentProps.ts +++ b/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() diff --git a/packages/runtime-core/src/componentRenderUtils.ts b/packages/runtime-core/src/componentRenderUtils.ts index 0bb1327ee..e5964d0db 100644 --- a/packages/runtime-core/src/componentRenderUtils.ts +++ b/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 */) ) }