vue3 源码学习:ref 模板引用原理

共 1217 字,预计阅读 7 分钟

ref 模板引用功能简介

根据官网的描述,ref 是一个特殊的 attribute,当一个 DOM 元素或者子组件实例被挂在后,能够直接获取到挂载元素的属性或者方法

<script setup>
  import { ref, onMounted } from 'vue'

  // 声明一个 ref 来存放该元素的引用
  // 必须和模板里的 ref 同名
  const input = ref(null)

  onMounted(() => {
    input.value.focus()
  })
</script>

<template>
  <input ref="input" />
</template>

所以简单来说,ref 模板引用就是直接进行 DOM 操作的一个方式,类似 getElementById(),用来完成一些 vue 数据驱动模型无法覆盖的场景。不过需要注意的是,对 DOM 操作一定是要在 DOM 渲染完成之后,所以在使用 ref 模板引用的过程中需要考虑到 DOM 不存在的情况

下面介绍一下 vue3 中 ref 模板引用的实现原理

实现原理

vue3 通过 createApp 方法创建 vue 实例并通过 mount 方法挂载 DOM 节点,在 mount 方法执行过程中,通过 createVNode 方法创建一个 vnode 节点,从最终生成 vnode 节点的 createBaseVNode 的方法中可以看到,ref 属性已经包含在创建的 vnode 对象中

// 函数参数已省略,具体函数目录在 packages/runtime-core/src/vnode.ts
function createBaseVNode() {
  const vnode = {
    type,
    props,
    key: props && normalizeKey(props),
    // ref 模板引用属性
    ref: props && normalizeRef(props),
    children,
    component: null,
    ctx: currentRenderingInstance,
    // ... 省略部分属性
  } as VNode

  // 返回一个 vnode 对象
  return vnode
}

通过 normalizeRef 方法创建了 ref 属性,可以看到在满足条件的情况下将 currentRenderingInstance 变量赋值给了 ref 属性

const normalizeRef = ({
  ref,
  ref_key,
  ref_for,
}: VNodeProps): VNodeNormalizedRefAtom | null => {
  return (
    ref != null
      ? isString(ref) || isRef(ref) || isFunction(ref)
        ? // 在满足条件的情况下将 currentRenderingInstance 变量赋值给了 ref 属性
          { i: currentRenderingInstance, r: ref, k: ref_key, f: !!ref_for }
        : ref
      : null
  ) as any
}

currentRenderingInstance 变量用于记录当前渲染的组件实例,通过 setCurrentRenderingInstance 方法来设置 currentRenderingInstance 变量,同时返回父组件的实例

export function setCurrentRenderingInstance(
  instance: ComponentInternalInstance | null
): ComponentInternalInstance | null {
  const prev = currentRenderingInstance
  // 记录当前组件实例
  currentRenderingInstance = instance
  // 返回父组件的组件实例
  return prev
}

setCurrentRenderingInstance 方法之所以会返回父组件的实例,是因为 vue3 渲染过程中会渲染父组件再渲染子组件,在渲染子组件过程中,当需要用到父组件实例时(比如通过 inject 获取依赖注入的值),就可以通过 setCurrentRenderingInstance 的返回值直接获取

在子组件渲染完成后,setCurrentRenderingInstance 会被再次调用并将当前的组件实例设置为父组件渲染实例,这样确保子组件在渲染完成之后,还能够正确获取到父组件的实例

renderComponentRoot 方法返回组件的 vnode,过程中设置了 currentRenderingInstanc 变量

export function renderComponentRoot(
  instance: ComponentInternalInstance
): VNode {
  let result

  // 设置当前正在渲染的组件实例
  const prev = setCurrentRenderingInstance(instance)

  // ... 执行组件的 setup 函数和 render 函数以生成组件的 VNode

  // 将当前的组件实例设置为父组件渲染实例
  setCurrentRenderingInstance(prev)

  return result // 返回组件的 VNode
}

renderComponentRoot 函数执行完成后,进入到 patch 方法将 vnode 转换为真实的 DOM,在 patch 函数执行的末尾,通过 setRef 方法来设置 ref 模板引用

const patch: PatchFn = (
  n1,
  n2,
  parentComponent = null,
  parentSuspense = null
  // 省略部分参数
) => {
  // ... 不同类型的 vnode 处理过程

  // 设置 ref 模板引用
  if (ref != null && parentComponent) {
    setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
  }
}

setRef 方法中对于 ref 类型有几种处理情况(源码中还有很多边界情况,这里只是最简化的场景)

  • 数组类型的 ref,遍历调用 setRef 方法处理
  • 函数类型的 ref,直接执行 ref 函数(vue3 中所有函数都通过 callWithErrorHandling 方法执行)
  • 字符串类型的 ref,将 ref 值 value 赋值给渲染上下文的 setupState 对象
export function setRef(
  rawRef: VNodeNormalizedRef,
  oldRawRef: VNodeNormalizedRef | null,
  parentSuspense: SuspenseBoundary | null,
  vnode: VNode,
  isUnmount = false
) {
  // 数组形式的 ref,遍历调用 setRef 方法
  if (isArray(rawRef)) {
    rawRef.forEach((r, i) =>
      setRef(
        r,
        oldRawRef && (isArray(oldRawRef) ? oldRawRef[i] : oldRawRef),
        parentSuspense,
        vnode,
        isUnmount
      )
    )
    return
  }

  // ! 如果是异步组件并且还没有挂载,直接返回
  if (isAsyncWrapper(vnode) && !isUnmount) {
    return
  }

  // 处理 ref 的值 value
  // ! 如果是组件,获取组件的实例,否则获取 vnode 的 元素
  const refValue =
    vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
      ? getExposeProxy(vnode.component!) || vnode.component!.proxy
      : vnode.el
  const value = isUnmount ? null : refValue

  const setupState = owner.setupState

  // 函数类型的 ref,执行 ref 函数
  if (isFunction(ref)) {
    callWithErrorHandling(ref, owner, ErrorCodes.FUNCTION_REF, [value, refs])
  } else if (_isString) {
    // 字符串类型的 ref,如果在对应渲染上下文存在 ref 的 key
    // 赋值 ref 值给渲染下文 setupState
    if (hasOwn(setupState, ref)) {
      setupState[ref] = value
    }
  }
}

最后再简单梳理一下 ref 模板引用的实现流程