Vue3 模板引用 ref 的实现原理
什么是模板引用 ref
?
有时候可以使用 ref
attribute 为子组件或 HTML 元素指定引用 ID。
<template>
<input ref="input" />
</template>
<script>
import { defineComponent, ref } from "vue";
export default defineComponent({
setup() {
const input = ref(null);
const focusInput = () => {
input.value.focus();
};
return {
input,
};
},
});
</script>
这里在渲染上下文中暴露 input
,并通过 ref="input"
,将其绑定到 input 作为其 ref。在虚拟 DOM 补丁算法中,如果 VNode 的 ref
键对应于渲染上下文中的 ref,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。这是在虚拟 DOM 挂载/打补丁过程中执行的,因此模板引用只会在初始渲染之后获得赋值。
设置当前渲染的实例对象
知道写的这个组件在运行的时候,先会创建一个组件实例对象instance
,再通过运行这个组件实例对象的render
方法获取这个组件的虚拟 DOM,然后再进行patch
,渲染出真实 DOM。
在运行组件实例对象的 render 方法之前,会先设置保存正在渲染的组件实例对象currentRenderingInstance
renderComponentRoot 方法
import { setCurrentRenderingInstance } from "./componentRenderContext";
export function renderComponentRoot(instance) {
const { proxy, render } = instance;
let result;
// 返回上一个实例对象
const prev = setCurrentRenderingInstance(instance);
result = render.call(proxy);
// 再设置当前的渲染对象上一个,具体场景是嵌套循环渲染的时候,渲染完子组件,再去渲染父组件
setCurrentRenderingInstance(prev);
return result;
}
setCurrentRenderingInstance 方法
export let currentRenderingInstance = null;
export function setCurrentRenderingInstance(instance) {
const prev = currentRenderingInstance;
currentRenderingInstance = instance;
return prev;
}
设置元素或者组件的 props 中的 ref
在获取组件的虚拟 DOM 的时候,其实是通过 createVNode 来创建的虚拟 DOM,在创建的虚拟 DOM 的时候会保存当前前渲染的实例对象到当前元素或者组件的 props 中的 ref 中。
export function createVNode(type, props?, children?) {
const vnode = {
type,
props,
ref: props && normalizeRef(props), // 创建虚拟DOM的时候设置ref
children,
component: null,
key: props && props.key,
shapeFlag: getShapeFlag(type),
el: null,
};
return vnode;
}
来看看 normalizeRef 函数做了什么
import { currentRenderingInstance } from "./componentRenderContext"
const normalizeRef = ({
ref
}) => {
return (
ref != null
? isString(ref) || isRef(ref) || isFunction(ref)
? { i: currentRenderingInstance, r: ref}
: ref
: null
) as any
}
可以看到 normalizeRef 函数最主要是把当前的渲染实例对象currentRenderingInstance
保存起来了。
模板引用的赋值
在上面开头的时候已经说了,模板引用 ref 只会在初始渲染之后获得。那么具体在源码中的位置是 patch 函数的底部,也就是把虚拟 DOM 进行 patch 渲染之后,再设置模版引用 ref。
function patch(n1, n2, container: any, parentComponent, anchor) {
// 基于 n2 的类型来判断
// 因为 n2 是新的 vnode
const { type, shapeFlag, ref } = n2;
// Fragment => 只渲染 children
switch (type) {
// 其中还有几个类型比如: static fragment comment
case Fragment:
processFragment(n1, n2, container, parentComponent, anchor);
break;
case Text:
processText(n1, n2, container);
break;
default:
// 这里就基于 shapeFlag 来处理
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理 element
processElement(n1, n2, container, parentComponent, anchor);
} else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
// 处理 component
processComponent(n1, n2, container, parentComponent, anchor);
}
break;
}
// 模板引用ref只会在初始渲染之后获得
if (ref != null && parentComponent) {
setRef(ref, n2 || n1, !n2);
}
}
再看看 setRef 函数中干了什么事情。
export function setRef(
rawRef,
vnode,
isUnmount = false
) {
// 判断如果是组件实例,则把改组件实例作为ref的值,否则就是把该元素作为ref值
const refValue =
vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
? vnode.component!.proxy
: vnode.el
// 如果n2不存在则是卸载
const value = isUnmount ? null : refValue
// 把在创建虚拟DOM的时候设置保存的组件渲染实例和ref键值解构出来
const { i: owner, r: ref } = rawRef
const setupState = owner.setupState
// happy path中只考虑最简单的情况
const _isString = isString(ref)
if (_isString) {
// 如果在对应于渲染上下文中存在ref键值,则 VNode 的相应元素或组件实例将被分配给该 ref 的值
if (hasOwn(setupState, ref)) {
setupState[ref] = value
}
}
}
模版引用 ref 的赋值具体就是在 setRef 函数中实现的。判断如果是组件实例,则把改组件实例作为 ref 的值,否则就是把该元素作为 ref 值,再把在创建虚拟 DOM 的时候设置保存的组件渲染实例和 ref 键值解构出来,再判断如果在对应于渲染上下文中存在 ref 键值,则 VNode 的相应元素或组件实例将被分配给该 ref 的值。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南