vue3中ref或useTemplateRef获取元素或组件是怎样实现的?

又是调试源码,学习vue3实现原理的一天,加油!大家一起卷!

  • ps:以下使用的是vue3.5.13版本

1.Ref获取元素或组件expose

1.1 基本用法

HelloWorld.vue

html 复制代码
<script setup lang="ts">
  import { ref } from 'vue';
  const props = defineProps<{ msg: string }>();
  const aRef = ref<number>(100);
  const say = () => {
    console.log(props.msg);
  };
  defineExpose({
    hahaha: '醒目',
    aRef,
    say
  });
</script>

<template>
  <h1>{{ msg }}</h1>
</template>

App.vue中,ref响应式变量名必须与ref属性值一致。

html 复制代码
<template>
  <div>
    <button @click="onChange()" ref="Btn">change</button>
    <p ref="smartRef">机智如我{{ countRef }}</p>
    <HelloWorld msg="HelloWorld" ref="helloRef"></HelloWorld>
  </div>
</template>

<script setup lang="ts">
  import HelloWorld from './components/HelloWorld.vue';
  import { onMounted, ref } from 'vue';
  const countRef = ref(0);
  let Btn = undefined;
  const smartRef = ref<HTMLParagraphElement>();
  const helloRef = ref<InstanceType<typeof HelloWorld>>();
  const onChange = () => {
    countRef.value++;
  };
  onMounted(() => {
    console.log(smartRef.value, helloRef.value, Btn);
  });
</script>

执行结果

TemplateRef.vue编译后setup函数

js 复制代码
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "TemplateRef",
  setup(__props, { expose: __expose }) {
    __expose();
    const countRef = ref(0);
    let Btn = void 0;
    const smartRef = ref();
    const helloRef = ref();
    const onChange = () => {
      countRef.value++;
    };
    onMounted(() => {
      console.log(smartRef.value, helloRef.value, Btn);
    });
    const __returned__ = { countRef, get Btn() {
      return Btn;
    }, set Btn(v) {
      Btn = v;
    }, smartRef, helloRef, onChange, HelloWorld };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});

TemplateRef.vue编译后模板的render函数,可以看到设置了ref属性的HelloWorld组件、P元素、Button元素都标记了NEED_PATCH

js 复制代码
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
  return _openBlock(), _createElementBlock("div", null, [
    _createElementVNode(
      "button",
      {
        onClick: $setup.onChange,
        ref: "Btn"
      },
      "change",
      512
      /* NEED_PATCH */
    ),
    _createElementVNode(
      "p",
      { ref: "smartRef" },
      "\u673A\u667A\u5982\u6211" + _toDisplayString($setup.countRef),
      513
      /* TEXT, NEED_PATCH */
    ),
    _createVNode(
      $setup["HelloWorld"],
      {
        msg: "HelloWorld",
        ref: "helloRef"
      },
      null,
      512
      /* NEED_PATCH */
    )
  ]);
}

1.2 ref获取真实DOM或组件的工作原理

  • patch函数中,先将虚拟DOM渲染成真实DOM并挂载在页面上,然后判断当前最新的虚拟DOMn2中是否配置了ref属性,并且所在的组件实例parentComponent已经已生成,若是,则通过setRef函数给该组件实例对应的setup函数返回的变量挂载组件expose信息或真实DOM。
ts 复制代码
 const patch: PatchFn = (n1,n2,container,anchor = null,parentComponent = null,parentSuspense = null,
   //...
  ) => {
 //...
  const { type, ref, shapeFlag } = n2;
  //...  
  if (ref != null && parentComponent) {
      setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
    }
 } 
  • mountElement中,虚拟DOMvNode渲染成真实DOM过程中,会给vNode.el属性挂上渲染后的真实DOM。
ts 复制代码
const mountElement = ( vnode: VNode,
   //...
 ) => {
 //...
 el = vnode.el = hostCreateElement(
     vnode.type as string,
     namespace,
     props && props.is,
     props,
   )
   //...
 }

setRef函数中,特殊情况需特殊处理:

  • 当存在多个相同ref属性值的元素或组件时,会将最后的那个元素或组件挂到变量的值上。这个跟vue2的不一样,vue2设置了相同ref属性值的元素或组件,可以通过this.$refs获取到设置了该ref属性的所有元素或组件的数组。
  • KeepAlive设置了ref属性,并且KeepAlive包裹了异步组件的情况,会判断异步组件是否已经生成组件实例,并将该异步组件实例挂在变量的值上。
ts 复制代码
export function setRef(
  rawRef: VNodeNormalizedRef,
  oldRawRef: VNodeNormalizedRef | null,
  parentSuspense: SuspenseBoundary | null,
  vnode: VNode,
  isUnmount = false,
): void {
  //...
    //KeepAlive包裹异步组件
  if (isAsyncWrapper(vnode) && !isUnmount) {     
    if (
      vnode.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE &&
      (vnode.type as ComponentOptions).__asyncResolved &&
      vnode.component!.subTree.component
    ) {
      setRef(rawRef, oldRawRef, parentSuspense, vnode.component!.subTree)
    }
    return
  }
  //获取ref.value的值
  const refValue =
    vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT
      ? getComponentPublicInstance(vnode.component!)
      : vnode.el
   //未渲染值为空
  const value = isUnmount ? null : refValue
    //i: owner是组件实例,r: ref是ref属性值
  const { i: owner, r: ref } = rawRef
  //...

当获取ref属性对象的值时,通过虚拟DOM的shapeFlag判断类型并分别获取,若是组件,则通过getComponentPublicInstance函数获取组件expose暴露出来的相关变量或函数,若是元素,则直接获取虚拟DOM渲染成的真实DOMvnode.el

ts 复制代码
export function getComponentPublicInstance(
  instance: ComponentInternalInstance,
): ComponentPublicInstance | ComponentInternalInstance['exposed'] | null {
  if (instance.exposed) {
    return (
      instance.exposeProxy ||
      (instance.exposeProxy = new Proxy(proxyRefs(markRaw(instance.exposed)), {
        get(target, key: string) {
          if (key in target) {
              //组合式API
            return target[key]
          } else if (key in publicPropertiesMap) {
           //选项式API
            return publicPropertiesMap[key](instance)
          }
        },
        has(target, key: string) {
          return key in target || key in publicPropertiesMap
        },
      }))
    )
  } else { 
    return instance.proxy
  }
}

我之前的文vue3中ref为什么script中要用.value,而template模板中不需?介绍过,setupState是组件实例的setup函数执行返回的所有变量和函数。而setupState是一个shallowUnwrapHandlers代理操作的响应式变量,可以直接通过变量名获取或修改ref.value值。

setRef中,通过canSetSetupRef函数判断setupState中是否有ref属性值命名的变量存在。

ts 复制代码
const oldRef = oldRawRef && (oldRawRef as VNodeNormalizedRefAtom).r
//refs用于缓存设置了ref属性的元素和组件expose
  const refs = owner.refs === EMPTY_OBJ ? (owner.refs = {}) : owner.refs
 const setupState = owner.setupState
  const rawSetupState = toRaw(setupState)
  const canSetSetupRef =
    setupState === EMPTY_OBJ
      ? () => false
      : (key: string) => {
          if (__DEV__) {
          //绑定的变量不是ref响应式变量,警告提示
            if (hasOwn(rawSetupState, key) && !isRef(rawSetupState[key])) {
              warn(`Template ref "${key}" used on a non-ref value. ` +
                  `It will not work in the production build.`)
            }
            if (knownTemplateRefs.has(rawSetupState[key] as any)) {
              return false
            }
          }
          return hasOwn(rawSetupState, key)
        }
        //动态更新,置空旧的设置了ref属性的元素和组件
   if (oldRef != null && oldRef !== ref) {
    if (isString(oldRef)) {
      refs[oldRef] = null
      if (canSetSetupRef(oldRef)) {
        setupState[oldRef] = null
      }
    } else if (isRef(oldRef)) {
      oldRef.value = null
    }
  }
  • setRef中,若设置了ref属性的组件expose或元素存在,则将设置ref.value值的任务函数doSet通过queuePostRenderEffect函数添加到渲染更新任务后的执行队列中,待组件渲染更新真实DOM完成再执行(为了解决获取不到异步组件的问题)。
  • setRef中,若设置了ref属性的对象值为空,则代表组件已经卸载或元素已经移除,直接执行doSet设置为空值。
ts 复制代码
    const _isString = isString(ref) 
    const _isRef = isRef(ref)
    if (_isString || _isRef) {
      const doSet = () => {
        //...
      }
      if (value) {
        (doSet as SchedulerJob).id = -1
        queuePostRenderEffect(doSet, parentSuspense)
      } else {
        doSet()
      }
    }
    //... 
  • doSet函数中,给组件实例的refs中缓存设置了ref属性值的子组件expose信息和元素真实DOM,再给zuj 实例的setupState对应的ref属性值变量赋值上子组件expose信息和元素真实DOM。
ts 复制代码
 const doSet = () => {
  //... 
        if (_isString) {
          refs[ref] = value
          if (canSetSetupRef(ref)) {
            setupState[ref] = value
          }
        }  
         //... 
      }

2.useTemplateRef获取元素或组件expose

2.1 基本用法

useTemplateRef官方说法:返回一个浅层 ref,其值将与模板中的具有匹配 ref attribute 的元素或组件同步。 TemplateRef1.vue,代码与上面的基本一致,除了使用useTemplateRef,并且useTemplateRef变量跟ref属性值不同。

html 复制代码
<template>
  <div>
    <button @click="onChange" ref="Btn">change</button>
    <p ref="smartRef">机智如我{{ countRef }}</p>
    <HelloWorld msg="HelloWorld" ref="helloRef"></HelloWorld>
  </div>
</template>

<script setup lang="ts">
  import HelloWorld from './components/HelloWorld.vue';
  import { onMounted, ref, useTemplateRef } from 'vue';
  const countRef = ref(0);
  let Btn: HTMLButtonElement;
  const smartRef1 = useTemplateRef<HTMLParagraphElement>('smartRef');
  const helloRef1 = useTemplateRef<InstanceType<typeof HelloWorld>>('helloRef');
  const onChange = () => {
    countRef.value++;
  };
  onMounted(() => {
    console.log(smartRef1.value, helloRef1.value, Btn);
  });
</script>

TemplateRef1.vue编译后setup函数

js 复制代码
const _sfc_main = /* @__PURE__ */ _defineComponent({
  __name: "TemplateRef1",
  setup(__props, { expose: __expose }) {
    __expose();
    const countRef = ref(0);
    let Btn;
    const smartRef1 = useTemplateRef("smartRef");
    const helloRef1 = useTemplateRef("helloRef");
    const onChange = () => {
      countRef.value++;
    };
    onMounted(() => {
      console.log(smartRef1.value, helloRef1.value, Btn);
    });
    const __returned__ = { countRef, get Btn() {
      return Btn;
    }, set Btn(v) {
      Btn = v;
    }, smartRef1, helloRef1, onChange, HelloWorld };
    Object.defineProperty(__returned__, "__isScriptSetup", { enumerable: false, value: true });
    return __returned__;
  }
});

执行结果与上面一致。

2.2 useTemplateRef的工作原理

useTemplateRef实际上返回的是一个值为nullshallowRef浅层响应式变量r,并且给当前组件实例的refs添加key关键词的数据劫持,gettersetter映射到浅层响应式变量r的操作。

从上面的setRef函数中可知组件实例的refs缓存了所有设置了ref属性的组件expose或元素,那么useTemplateRef添加劫持可以通过refskey关键词获取或修改浅层响应式变量r的值。

这样可以解除setup函数返回的响应式变量与设置ref属性值必须一致性的限制,只需key关键词与ref属性值相同即可,使得响应式变量命名更灵活。

ts 复制代码
export function useTemplateRef<T = unknown, Keys extends string = string>(
  key: Keys,
): Readonly<ShallowRef<T | null>> {
  const i = getCurrentInstance()
  const r = shallowRef(null)
  if (i) {
    const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs
    let desc: PropertyDescriptor | undefined
    if (
      __DEV__ &&
      (desc = Object.getOwnPropertyDescriptor(refs, key)) &&
      !desc.configurable
    ) {
      warn(`useTemplateRef('${key}') already exists.`)
    } else {
      Object.defineProperty(refs, key, {
        enumerable: true,
        get: () => r.value,
        set: val => (r.value = val),
      })
    }
  } else if (__DEV__) {
    warn(
      `useTemplateRef() is called when there is no active component ` +
        `instance to be associated with.`,
    )
  }
  const ret = __DEV__ ? readonly(r) : r
  if (__DEV__) {
    knownTemplateRefs.add(ret)
  }
  return ret
}

组件实例每次渲染更新,设置了ref属性的每个组件每个元素在patch函数中都需要进行一次setRef更新挂载的元素或组件expose,即便元素或组件expose的引用未改变也要重新赋值。一旦设置了ref属性的节点过多,就会很影响性能!

额~为什么没有类似patchFlag的标记用来记录元素或组件expose的引用是否改变,从而跳过setRef操作来优化执行流程。

然后,我在useTemplateRef中注意到一个特殊的变量knownTemplateRefs,一个WeakSet

  • 开发环境时,knownTemplateRefs缓存了shallowRef浅层响应式变量r,在patch更新时,setRefcanSetSetupRef通过判断knownTemplateRefs中是否ref属性值有缓存,从而跳过重新赋值(只适用于useTemplateRef变量名与ref属性值一致的情况)。

可见,vue团队也注意到这个setRef频繁重新赋值的问题,并正在优化中!我猜,推出useTemplateRef是为解决setRef频繁赋值的问题做准备。

3.总结

那么现在可以回答了!

ref获取元素或组件是怎样实现的?

回答:

  1. patch函数中,会给设置了ref属性的虚拟DOM进行setRef操作。
  2. setRef中,判断设置了ref属性的虚拟DOM是组件还是元素,若是组件则获取组件实例的expose值,若是元素则直接获取真实DOMvnode.el
  3. 若设置了ref属性虚拟DOM的值不为空,则将【变量赋值】的任务,加入到渲染更新任务后执行队列。
  4. 待渲染更新任务完成,执行赋值,将元素真实DOM或子组件expose信息挂载在组件实例setupState的ref属性值命名的变量上。

useTemplateRef相对比ref获取元素或组件的方式有什么不同?

  1. API使用方式不同,两者回类型不一样,ref是深层响应式,useTemplateRef返回的是浅层refkey值与ref属性值一致即可,变量名可以不一样,而ref响应式变量需与对象的ref属性值一致。
  2. patch过程中,通过setRef操作将组件的expose或真实DOM挂在ref响应式变量的.value上,useTemplateRef多了对refs缓存的利用。
  3. useTemplateRef利用对组件实例缓存的refskey关键词劫持,来操作浅层ref,从而减少setup函数中对响应式变量命名的限制。

之前,关于vue3源码理解的笔记,感兴趣的小伙伴可以瞅瞅

相关推荐
binqian30 分钟前
【异步】js中异步的实现方式 async await /Promise / Generator
开发语言·前端·javascript
LIUENG1 小时前
Vue3 响应式原理
前端·vue.js
前端李二牛1 小时前
异步任务并发控制
前端·javascript
你也向往长安城吗1 小时前
推荐一个三维导航库:three-pathfinding-3d
javascript·算法
karrigan2 小时前
async/await 的优雅外衣下:Generator 的核心原理与 JavaScript 执行引擎的精细管理
javascript
wycode2 小时前
Vue2实践(3)之用component做一个动态表单(二)
前端·javascript·vue.js
wycode3 小时前
Vue2实践(2)之用component做一个动态表单(一)
前端·javascript·vue.js
第七种黄昏3 小时前
Vue3 中的 ref、模板引用和 defineExpose 详解
前端·javascript·vue.js
我是哈哈hh3 小时前
【Node.js】ECMAScript标准 以及 npm安装
开发语言·前端·javascript·node.js
张元清4 小时前
电商 Feeds 流缓存策略:Temu vs 拼多多的技术选择
前端·javascript·面试