vue3 源码学习:ref 模板引用原理
ref 模板引用功能简介
根据官网的描述,ref
是一个特殊的 attribute,当一个 DOM 元素或者子组件实例被挂在后,能够直接获取到挂载元素的属性或者方法
1<script setup>
2 import { ref, onMounted } from 'vue'
3
4 // 声明一个 ref 来存放该元素的引用
5 // 必须和模板里的 ref 同名
6 const input = ref(null)
7
8 onMounted(() => {
9 input.value.focus()
10 })
11</script>
12
13<template>
14 <input ref="input" />
15</template>
所以简单来说,ref 模板引用就是直接进行 DOM 操作的一个方式,类似 getElementById()
,用来完成一些 vue 数据驱动模型无法覆盖的场景。不过需要注意的是,对 DOM 操作一定是要在 DOM 渲染完成之后,所以在使用 ref 模板引用的过程中需要考虑到 DOM 不存在的情况
下面介绍一下 vue3 中 ref 模板引用的实现原理
实现原理
vue3 通过 createApp
方法创建 vue 实例并通过 mount
方法挂载 DOM 节点,在 mount
方法执行过程中,通过 createVNode
方法创建一个 vnode 节点,从最终生成 vnode 节点的 createBaseVNode
的方法中可以看到,ref 属性已经包含在创建的 vnode 对象中
1// 函数参数已省略,具体函数目录在 packages/runtime-core/src/vnode.ts
2function createBaseVNode() {
3 const vnode = {
4 type,
5 props,
6 key: props && normalizeKey(props),
7 // ref 模板引用属性
8 ref: props && normalizeRef(props),
9 children,
10 component: null,
11 ctx: currentRenderingInstance,
12 // ... 省略部分属性
13 } as VNode
14
15 // 返回一个 vnode 对象
16 return vnode
17}
通过 normalizeRef
方法创建了 ref 属性,可以看到在满足条件的情况下将 currentRenderingInstance 变量赋值给了 ref 属性
1const normalizeRef = ({
2 ref,
3 ref_key,
4 ref_for,
5}: VNodeProps): VNodeNormalizedRefAtom | null => {
6 return (
7 ref != null
8 ? isString(ref) || isRef(ref) || isFunction(ref)
9 ? // 在满足条件的情况下将 currentRenderingInstance 变量赋值给了 ref 属性
10 { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
11 : ref
12 : null
13 ) as any
14}
currentRenderingInstance 变量用于记录当前渲染的组件实例,通过 setCurrentRenderingInstance
方法来设置 currentRenderingInstance 变量,同时返回父组件的实例
1export function setCurrentRenderingInstance(
2 instance: ComponentInternalInstance | null
3): ComponentInternalInstance | null {
4 const prev = currentRenderingInstance
5 // 记录当前组件实例
6 currentRenderingInstance = instance
7 // 返回父组件的组件实例
8 return prev
9}
setCurrentRenderingInstance
方法之所以会返回父组件的实例,是因为 vue3 渲染过程中会渲染父组件再渲染子组件,在渲染子组件过程中,当需要用到父组件实例时(比如通过 inject 获取依赖注入的值),就可以通过 setCurrentRenderingInstance
的返回值直接获取
在子组件渲染完成后,setCurrentRenderingInstance
会被再次调用并将当前的组件实例设置为父组件渲染实例,这样确保子组件在渲染完成之后,还能够正确获取到父组件的实例
renderComponentRoot
方法返回组件的 vnode,过程中设置了 currentRenderingInstanc 变量
1export function renderComponentRoot(
2 instance: ComponentInternalInstance
3): VNode {
4 let result
5
6 // 设置当前正在渲染的组件实例
7 const prev = setCurrentRenderingInstance(instance)
8
9 // ... 执行组件的 setup 函数和 render 函数以生成组件的 VNode
10
11 // 将当前的组件实例设置为父组件渲染实例
12 setCurrentRenderingInstance(prev)
13
14 return result // 返回组件的 VNode
15}
在 renderComponentRoot
函数执行完成后,进入到 patch
方法将 vnode 转换为真实的 DOM,在 patch 函数执行的末尾,通过 setRef
方法来设置 ref 模板引用
1const patch: PatchFn = (
2 n1,
3 n2,
4 parentComponent = null,
5 parentSuspense = null
6 // 省略部分参数
7) => {
8 // ... 不同类型的 vnode 处理过程
9
10 // 设置 ref 模板引用
11 if (ref != null && parentComponent) {
12 setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
13 }
14}
在 setRef
方法中对于 ref 类型有几种处理情况(源码中还有很多边界情况,这里只是最简化的场景)
- 数组类型的 ref,遍历调用
setRef
方法处理 - 函数类型的 ref,直接执行 ref 函数(vue3 中所有函数都通过
callWithErrorHandling
方法执行) - 字符串类型的 ref,将 ref 值 value 赋值给渲染上下文的 setupState 对象
1export function setRef(
2 rawRef: VNodeNormalizedRef,
3 oldRawRef: VNodeNormalizedRef | null,
4 parentSuspense: SuspenseBoundary | null,
5 vnode: VNode,
6 isUnmount = false
7) {
8 // 数组形式的 ref,遍历调用 setRef 方法
9 if (isArray(rawRef)) {
10 rawRef.forEach((r, i) =>
11 setRef(
12 r,
13 oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
14 parentSuspense,
15 vnode,
16 isUnmount
17 )
18 )
19 return
20 }
21
22 // ! 如果是异步组件并且还没有挂载,直接返回
23 if (isAsyncWrapper(vnode) && !isUnmount) {
24 return
25 }
26
27 // 处理 ref 的值 value
28 // ! 如果是组件,获取组件的实例,否则获取 vnode 的 元素
29 const refValue =
30 vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
31 ? getExposeProxy(vnode.component!) || vnode.component!.proxy
32 : vnode.el
33 const value = isUnmount ? null : refValue
34
35 const setupState = owner.setupState
36
37 // 函数类型的 ref,执行 ref 函数
38 if (isFunction(ref)) {
39 callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
40 } else if (_isString) {
41 // 字符串类型的 ref,如果在对应渲染上下文存在 ref 的 key
42 // 赋值 ref 值给渲染下文 setupState
43 if (hasOwn(setupState, ref)) {
44 setupState[ref] = value
45 }
46 }
47}
最后再简单梳理一下 ref 模板引用的实现流程