feat(expose): always expose $ instance properties on child refs

This commit is contained in:
Evan You 2021-06-24 21:28:09 -04:00
parent a5a66c5196
commit b0203a3092
5 changed files with 75 additions and 29 deletions

View File

@ -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')
})
})

View File

@ -14,7 +14,8 @@ import {
createRenderContext,
exposePropsOnRenderContext,
exposeSetupStateOnRenderContext,
ComponentPublicInstanceConstructor
ComponentPublicInstanceConstructor,
publicPropertiesMap
} from './componentPublicInstance'
import {
ComponentPropsOptions,
@ -169,7 +170,7 @@ export interface SetupContext<E = EmitsOptions> {
attrs: Data
slots: Slots
emit: EmitFn<E>
expose: (exposed: Record<string, any>) => void
expose: (exposed?: Record<string, any>) => void
}
/**
@ -291,6 +292,7 @@ export interface ComponentInternalInstance {
// exposed properties via expose()
exposed: Record<string, any> | null
exposeProxy: Record<string, any> | 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(

View File

@ -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 = {}
}
}

View File

@ -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)

View File

@ -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