diff --git a/packages/runtime-core/__tests__/apiExpose.spec.ts b/packages/runtime-core/__tests__/apiExpose.spec.ts index a74edfad543..fb957b68db3 100644 --- a/packages/runtime-core/__tests__/apiExpose.spec.ts +++ b/packages/runtime-core/__tests__/apiExpose.spec.ts @@ -7,7 +7,7 @@ describe('api: expose', () => { render() {}, setup(_, { expose }) { expose({ - foo: ref(1), + foo: 1, bar: ref(2) }) return { @@ -169,4 +169,26 @@ describe('api: expose', () => { const root = nodeOps.createElement('div') render(h(Parent), root) }) + + test('expose should allow access to built-in instance properties', () => { + const Child = defineComponent({ + render() { + return h('div') + }, + setup(_, { expose }) { + expose() + return {} + } + }) + + const childRef = ref() + const Parent = { + setup() { + return () => h(Child, { ref: childRef }) + } + } + const root = nodeOps.createElement('div') + render(h(Parent), root) + expect(childRef.value.$el.tag).toBe('div') + }) }) diff --git a/packages/runtime-core/src/component.ts b/packages/runtime-core/src/component.ts index 6766e276a0c..01717af2052 100644 --- a/packages/runtime-core/src/component.ts +++ b/packages/runtime-core/src/component.ts @@ -14,7 +14,8 @@ import { createRenderContext, exposePropsOnRenderContext, exposeSetupStateOnRenderContext, - ComponentPublicInstanceConstructor + ComponentPublicInstanceConstructor, + publicPropertiesMap } from './componentPublicInstance' import { ComponentPropsOptions, @@ -169,7 +170,7 @@ export interface SetupContext { attrs: Data slots: Slots emit: EmitFn - expose: (exposed: Record) => void + expose: (exposed?: Record) => void } /** @@ -291,6 +292,7 @@ export interface ComponentInternalInstance { // exposed properties via expose() exposed: Record | null + exposeProxy: Record | null /** * alternative proxy used only for runtime-compiled render functions using @@ -447,6 +449,7 @@ export function createComponentInstance( render: null, proxy: null, exposed: null, + exposeProxy: null, withProxy: null, effects: null, provides: parent ? parent.provides : Object.create(appContext.provides), @@ -837,7 +840,7 @@ export function createSetupContext( if (__DEV__ && instance.exposed) { warn(`expose() should be called only once per setup().`) } - instance.exposed = proxyRefs(exposed) + instance.exposed = exposed || {} } if (__DEV__) { @@ -868,6 +871,23 @@ export function createSetupContext( } } +export function getExposeProxy(instance: ComponentInternalInstance) { + if (instance.exposed) { + return ( + instance.exposeProxy || + (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), { + get(target, key: string) { + if (key in target) { + return target[key] + } else if (key in publicPropertiesMap) { + return publicPropertiesMap[key](instance) + } + } + })) + ) + } +} + // record effects created during a component's setup() so that they can be // stopped when the component unmounts export function recordInstanceBoundEffect( diff --git a/packages/runtime-core/src/componentOptions.ts b/packages/runtime-core/src/componentOptions.ts index c6398378ec8..5c7960d341c 100644 --- a/packages/runtime-core/src/componentOptions.ts +++ b/packages/runtime-core/src/componentOptions.ts @@ -14,7 +14,6 @@ import { isString, isObject, isArray, - EMPTY_OBJ, NOOP, isPromise } from '@vue/shared' @@ -45,9 +44,7 @@ import { import { reactive, ComputedGetter, - WritableComputedOptions, - proxyRefs, - toRef + WritableComputedOptions } from '@vue/reactivity' import { ComponentObjectPropsOptions, @@ -540,7 +537,7 @@ export let shouldCacheAccess = true export function applyOptions(instance: ComponentInternalInstance) { const options = resolveMergedOptions(instance) - const publicThis = instance.proxy! + const publicThis = instance.proxy! as any const ctx = instance.ctx // do not cache property access on public proxy during state initialization @@ -773,12 +770,15 @@ export function applyOptions(instance: ComponentInternalInstance) { if (isArray(expose)) { if (expose.length) { - const exposed = instance.exposed || (instance.exposed = proxyRefs({})) + const exposed = instance.exposed || (instance.exposed = {}) expose.forEach(key => { - exposed[key] = toRef(publicThis, key as any) + Object.defineProperty(exposed, key, { + get: () => publicThis[key], + set: val => (publicThis[key] = val) + }) }) } else if (!instance.exposed) { - instance.exposed = EMPTY_OBJ + instance.exposed = {} } } diff --git a/packages/runtime-core/src/componentPublicInstance.ts b/packages/runtime-core/src/componentPublicInstance.ts index 83ab4521e0d..9206b974ba4 100644 --- a/packages/runtime-core/src/componentPublicInstance.ts +++ b/packages/runtime-core/src/componentPublicInstance.ts @@ -221,22 +221,25 @@ const getPublicInstance = ( return getPublicInstance(i.parent) } -const publicPropertiesMap: PublicPropertiesMap = extend(Object.create(null), { - $: i => i, - $el: i => i.vnode.el, - $data: i => i.data, - $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props), - $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs), - $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots), - $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs), - $parent: i => getPublicInstance(i.parent), - $root: i => getPublicInstance(i.root), - $emit: i => i.emit, - $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), - $forceUpdate: i => () => queueJob(i.update), - $nextTick: i => nextTick.bind(i.proxy!), - $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) -} as PublicPropertiesMap) +export const publicPropertiesMap: PublicPropertiesMap = extend( + Object.create(null), + { + $: i => i, + $el: i => i.vnode.el, + $data: i => i.data, + $props: i => (__DEV__ ? shallowReadonly(i.props) : i.props), + $attrs: i => (__DEV__ ? shallowReadonly(i.attrs) : i.attrs), + $slots: i => (__DEV__ ? shallowReadonly(i.slots) : i.slots), + $refs: i => (__DEV__ ? shallowReadonly(i.refs) : i.refs), + $parent: i => getPublicInstance(i.parent), + $root: i => getPublicInstance(i.root), + $emit: i => i.emit, + $options: i => (__FEATURE_OPTIONS_API__ ? resolveMergedOptions(i) : i.type), + $forceUpdate: i => () => queueJob(i.update), + $nextTick: i => nextTick.bind(i.proxy!), + $watch: i => (__FEATURE_OPTIONS_API__ ? instanceWatch.bind(i) : NOOP) + } as PublicPropertiesMap +) if (__COMPAT__) { installCompatInstanceProperties(publicPropertiesMap) diff --git a/packages/runtime-core/src/renderer.ts b/packages/runtime-core/src/renderer.ts index 66319fce3b8..1d1ebe77274 100644 --- a/packages/runtime-core/src/renderer.ts +++ b/packages/runtime-core/src/renderer.ts @@ -19,6 +19,7 @@ import { ComponentOptions, createComponentInstance, Data, + getExposeProxy, setupComponent } from './component' import { @@ -335,7 +336,7 @@ export const setRef = ( const refValue = vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT - ? vnode.component!.exposed || vnode.component!.proxy + ? getExposeProxy(vnode.component!) || vnode.component!.proxy : vnode.el const value = isUnmount ? null : refValue