Vue3 模板引用 ref 的实现原理

发布时间 2023-11-17 16:12:38作者: 柯基与佩奇

什么是模板引用 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 的值。