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源码理解的笔记,感兴趣的小伙伴可以瞅瞅

相关推荐
诸葛亮的芭蕉扇2 小时前
Vue3核心编译库@vuecompiler-core内容分享
前端·javascript·vue.js
Hopebearer_3 小时前
Vue3生命周期以及与Vue2的区别
前端·javascript·vue.js·前端框架·vue3
雅望天堂i4 小时前
vue的diff算法
前端·javascript·vue.js
爱上妖精的尾巴4 小时前
3-5 WPS JS宏 工作表的移动与复制学习笔记
javascript·笔记·学习·wps·js宏·jsa
计算机-秋大田4 小时前
基于Spring Boot的乡村养老服务管理系统设计与实现(LW+源码+讲解)
java·vue.js·spring boot·后端·课程设计
B站计算机毕业设计超人6 小时前
计算机毕业设计SpringBoot+Vue.js校园失物招领系统(源码+文档+PPT+讲解)
java·vue.js·spring boot·后端·毕业设计·课程设计·毕设
Enti7c6 小时前
什么是 jQuery
前端·javascript·jquery
计算机-秋大田6 小时前
基于SpringBoot的环保网站的设计与实现(源码+SQL脚本+LW+部署讲解等)
java·vue.js·spring boot·后端·课程设计
cafehaus6 小时前
关于JavaScript性能问题的误解
开发语言·javascript·ecmascript
taopi20247 小时前
若依vue plus环境搭建
前端·javascript·vue.js