Vue 3.5 useTemplateRef 实现原理剖析学习笔记
1. useTemplateRef 是什么?
useTemplateRef 是 Vue 3.5 版本中新增的一个 API,旨在提供一种更直观、更灵活的方式来获取模板(template)中的 DOM 元素引用。
2. 背景:Vue 3.5 之前的模板引用方式
在 Vue 3.5 之前,我们获取模板中 DOM 元素的方式如下:
- 在模板中的 DOM 元素上设置一个
ref属性,其值为一个字符串。 - 在
<script setup>中,必须声明一个 同名的ref变量 来接收这个 DOM 元素的引用。
示例代码:
<template>
<div ref="elRef">这是一个DOM元素</div>
</template>
<script setup>
import { ref, onMounted } from 'vue';
// 这里的变量名 "elRef" 必须和模板中的 ref="elRef" 完全一致
const elRef = ref(null);
onMounted(() => {
console.log(elRef.value); // 打印出 <div> 元素
});
</script>这种方式的痛点:
- 强耦合:
<script>中的变量名和模板中的字符串值被强行绑定在一起,不够灵活,感觉有些"奇怪"。 - 命名限制:如果模板中
ref的名字改了,脚本中的变量名也必须手动同步修改。
3. useTemplateRef 的优势与直观设计
useTemplateRef 解决了上述痛点,它提供了一种类似于 DOM 查询(如 querySelector)的体验。
示例代码:
<template>
<div ref="elRef">这是一个DOM元素</div>
</template>
<script setup>
import { useTemplateRef } from 'vue'; // 假设这是官方API
// 变量名 'container' 可以随意命名
// useTemplateRef 接收模板 ref 的 key 作为参数
const container = useTemplateRef('elRef');
// container 是一个 ref 对象,其 .value 会在挂载后指向 DOM 元素
onMounted(() => {
console.log(container.value); // 打印出 <div> 元素
});
</script>优势:
- 解耦:脚本中的变量名(如
container)与模板中的ref字符串(elRef)完全解耦。 - 直观:逻辑更清晰,明确表达了"我想获取名为 'elRef' 的模板引用"这一意图。
4. 核心实现:一步步构建我们自己的 useTemplateRef
手动实现一个 useTemplateRef,这能帮助我们深入理解其工作原理。
第一步:基本结构
函数接收一个 key (字符串),并返回一个 ref 对象。
import { shallowRef } from 'vue';
export function myUseTemplateRef(key) {
const container = shallowRef(null);
// ? 如何将 DOM 元素填充到 container.value 中?
return container;
}第二步:找到 Vue 内部的关联机制
- 关键入口点: Vue 的组合式 API 提供了
getCurrentInstance()方法,它可以获取当前组件的实例。 instance.refs: 在组件实例上,有一个refs对象。这个对象存储了所有模板ref的映射关系,即{ ref名: DOM元素 }。这正是 Vue 2 中this.$refs的本质。
我们可以利用 instance.refs 来建立连接。
第三步:初次尝试 - 使用 Object.defineProperty
我们的思路是:当 Vue 内部给 instance.refs 对象设置属性(如 instance.refs.elRef = <div元素>)时,我们通过 Object.defineProperty 拦截这个 set 操作,从而捕获到 DOM 元素。
import { shallowRef, getCurrentInstance } from 'vue';
export function myUseTemplateRef(key) {
const container = shallowRef(null);
const vm = getCurrentInstance(); // 获取组件实例
if (vm) {
Object.defineProperty(vm.refs, key, {
get() {
return container.value;
},
set(value) { // value 就是 Vue 传入的 DOM 元素
console.log('拦截到了 set 操作:', value);
container.value = value;
}
});
}
return container;
}第四步:遇到问题与解决方案
问题一:Uncaught TypeError: Cannot define property ..., object is not extensible
原因: 这是一个 Vue 的性能优化策略。对于绝大多数不使用模板
ref的组件,Vue 不会为每个实例都创建一个新的空refs对象({}),因为即使是空对象也会占用内存。取而代之的是,所有这些实例的refs属性都指向一个全局共享的、被冻结(Object.freeze())的空对象。被冻结的对象是不可扩展的,因此无法对其使用Object.defineProperty。解决方案: 在定义属性前,检查
vm.refs是否是那个被冻结的共享对象。如果是,就将其替换为一个新的、可写的普通空对象。
// Vue 源码中的逻辑
if (vm.refs === EMPTY_OBJ) { // EMPTY_OBJ 是 Vue 内部的共享冻结对象
vm.refs = {};
}
// 我们的模拟实现 (因为无法访问 EMPTY_OBJ)
// 直接替换即可
vm.refs = {};问题二:多次调用 useTemplateRef 导致监听被覆盖
场景: 在同一个组件中,如果调用两次
useTemplateRef:javascriptconst container1 = myUseTemplateRef('ref1'); const container2 = myUseTemplateRef('ref2');问题: 第二次调用
myUseTemplateRef时,会执行vm.refs = {},这会创建一个全新的对象,导致第一次调用时在旧对象上设置的defineProperty监听器失效。解决方案: 必须确保
vm.refs = {}这个初始化操作对于每个组件实例只执行一次。实现: 使用
WeakSet来记录哪些组件实例已经初始化过refs对象。WeakSet很适合这个场景,因为它对 V8 垃圾回收友好,当组件实例被销毁后,WeakSet中的引用会自动消失,不会造成内存泄漏。
第五步:最终实现
结合以上所有解决方案,我们得到了一个健壮的 useTemplateRef 实现。
import { shallowRef, getCurrentInstance } from 'vue';
// 使用 WeakSet 确保每个组件实例的 refs 对象只被初始化一次
const initialedInstances = new WeakSet();
export function useTemplateRef(key) {
const container = shallowRef(null);
const vm = getCurrentInstance();
if (vm) {
// 检查当前实例是否已经初始化过
if (!initialedInstances.has(vm)) {
// 如果没有,将其 refs 替换为一个新对象,并进行标记
vm.refs = {};
initialedInstances.add(vm);
}
Object.defineProperty(vm.refs, key, {
// 当外部代码访问 vm.refs.xxx 时,返回我们自己的 container.value
get() {
return container.value;
},
// 当 Vue 挂载并设置 DOM 元素时,将其存入我们的 container
set(value) {
container.value = value;
},
});
}
return container;
}5. 总结
useTemplateRef 的实现巧妙地利用了 Vue 内部的 instance.refs 机制。其核心是拦截和代理。
- 获取实例: 通过
getCurrentInstance()拿到组件实例。 - 处理优化: 识别并处理 Vue 为性能优化的 "共享冻结
refs对象" 的情况,通过WeakSet确保只对refs对象初始化一次,避免了多次调用时的相互覆盖。 - 拦截代理: 使用
Object.defineProperty监听特定refkey 的get和set操作,将 Vue 内部的赋值操作代理到我们自己创建的shallowRef(container) 上,最终返回这个ref对象给用户。
通过这个过程,我们不仅学会了 useTemplateRef 的用法,更深入地理解了 Vue 在性能优化和内部机制上的一些精妙设计。