通过 expose 暴露组件属性,并使用 ref 获取元素和组件实例
前面已经实现了组件的 attrs、emit 和 slots。
除此之外,组件还可以通过 expose 主动暴露一部分内部状态和方法,父组件再通过 ref 获取这些公开内容。
import { createApp, h, ref } from '../dist/vue.esm.js'
const Child = {
setup(_, { expose }) {
const count = ref(0)
function increment() {
count.value++
}
expose({
count,
increment
})
return () => {
return h('div', `count: ${count.value}`)
}
}
}
const App = {
setup() {
const childRef = ref(null)
function handleClick() {
childRef.value.increment()
}
return () => {
return h('div', [
h(Child, { ref: childRef }),
h(
'button',
{
onClick: handleClick
},
'increment'
)
])
}
}
}
createApp(App).mount('#app')
expose:决定组件实例对外公开哪些属性和方法ref:在组件挂载完成后,保存元素节点或组件公开实例
如果 ref 指向普通元素,得到的是元素节点;如果 ref 指向组件,得到的是组件公开实例。
实现 expose
expose 和 attrs、emit、slots 一样,都是通过 setup 的第二个参数传入的。
function createSetupContext(instance) {
return {
get attrs() {
return instance.attrs
},
emit(event, ...args) {
emit(instance, event, ...args)
},
slots: instance.slots,
expose(exposed) {
instance.exposed = exposed
}
}
}
调用 expose 时,只需要把传入的对象保存到组件实例上:
expose({
count,
increment
})
对应的组件实例会保存:
instance.exposed = {
count,
increment
}
expose 不会把组件内部的所有状态都暴露出去,而是由组件主动决定公开的内容。
实现 ref
1. 在 vnode 上保存 ref
父组件使用 ref 时,ref 会作为 vnode 的一部分传递下来:
h(Child, {
ref: childRef
})
所以,在创建 vnode 时需要保存 props.ref:
export function createVNode(type, props = null, children = null) {
const vnode = {
type,
props,
children,
ref: props?.ref
}
return vnode
}
但是,在设置 ref 时不仅需要知道原始的 ref,还需要知道这个 ref 属于哪个组件。
return () => h(Child, { ref: childRef })
上面这个ref 是在父组件的 render 中创建的,应该保存到父组件的 refs 中,而不是保存到子组件自身。
所以,在执行组件 render 时,需要记录当前正在渲染的组件实例。
2. 保存当前渲染实例
在组件开始执行 render 前保存实例,执行结束后清空实例:
// packages/runtime-core/src/componentRenderContext.ts
let currentRenderingInstance = null
export function setCurrentRenderingInstance(instance) {
currentRenderingInstance = instance
}
export function unsetCurrentRenderingInstance() {
currentRenderingInstance = null
}
export function getCurrentRenderingInstance() {
return currentRenderingInstance
}
在组件更新函数中使用:
function componentUpdateFn() {
setCurrentRenderingInstance(instance)
let subTree
try {
subTree = instance.render.call(instance.proxy)
}
finally {
unsetCurrentRenderingInstance()
}
// 首次挂载或后续更新
patch(instance.subTree, subTree, container, anchor)
instance.subTree = subTree
}
创建 vnode 时,将原始 ref 和当前渲染实例一起保存:
import { getCurrentRenderingInstance } from './componentRenderContext'
export function createVNode(type, props = null, children = null) {
const vnode = {
type,
props,
children,
ref: normalizeRef(props?.ref)
}
return vnode
}
function normalizeRef(rawRef) {
if (rawRef == null) {
return null
}
return {
// 原始 ref,可以是字符串或 Ref 对象
r: rawRef,
// 创建这个 vnode 时正在渲染的组件实例
i: getCurrentRenderingInstance()
}
}
在挂载完成后设置 ref
ref 对应的元素或组件实例,只有在挂载完成后才真正可用。
因此,应该在 patch 完成对应节点的挂载后设置 ref:
function patch(n1, n2, container, anchor = null) {
const { shapeFlag, ref } = n2
// 根据节点类型完成挂载或更新
// ...
if (ref != null) {
setRef(ref, n2)
}
}
setRef 需要区分两种情况:
ref指向普通元素,保存vnode.elref指向组件,保存组件的公开实例
// packages/runtime-core/src/rendererTemplateRef.ts
import { isRef, proxyRefs } from '@vue/reactivity'
import { isString, ShapeFlags } from '@vue/shared'
export function setRef(ref, vnode) {
const { r: rawRef, i: instance } = ref
const { shapeFlag } = vnode
const value = shapeFlag & ShapeFlags.COMPONENT
? getComponentPublicInstance(vnode.component)
: vnode.el
if (isRef(rawRef)) {
rawRef.value = value
}
else if (isString(rawRef) && instance) {
instance.refs[rawRef] = value
}
}
instance 就是创建 vnode 时记录的父组件实例。
获取组件公开实例
普通元素可以直接保存 vnode.el。
但组件 vnode 本身并不是一个真实的 DOM 元素,组件 ref 应该返回什么,由 getComponentPublicInstance 统一处理。
export function getComponentPublicInstance(instance) {
if (instance.exposed) {
if (instance.exposedProxy) {
return instance.exposedProxy
}
instance.exposedProxy = new Proxy(
proxyRefs(instance.exposed),
{
get(target, key) {
if (key in target) {
return target[key]
}
if (key in publicPropertiesMap) {
return publicPropertiesMap[key](instance)
}
}
}
)
return instance.exposedProxy
}
return instance.proxy
}
组件调用了 expose
如果组件调用了 expose,就通过 instance.exposed 创建一个代理对象。
代理对象有两个作用:
- 优先访问组件通过
expose暴露的属性 - 继续支持
$el、$attrs、$slots和$refs等公共属性
组件没有调用 expose
如果组件没有显式调用 expose,则返回之前创建的 instance.proxy。
这样可以保持组件 ref 的默认行为,同时让显式调用 expose 的组件拥有更明确的公开接口。
小结
expose通过setupContext提供给组件- 调用
expose时,将公开对象保存到instance.exposed ref会保存到 vnode 上,并记录原始值和所属的组件实例- 执行组件
render时,需要保存当前正在渲染的组件实例 - 节点挂载完成后,普通元素的
ref指向vnode.el - 组件的
ref指向getComponentPublicInstance返回的公开实例 expose创建的公开实例会优先暴露显式公开的属性proxyRefs让公开对象中的ref可以直接读取