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 模板引用的实现流程