牛逼!Vue3.5的useTemplateRef让ref操作DOM更加丝滑
前言
在 Vue 3 中,访问 DOM 和子组件可以通过 ref
进行模板引用,但这个 ref
有时会让人迷惑。比如,定义的 ref
变量到底是一个响应式数据还是 DOM 元素?为什么 template
中 ref
属性的值是一个字符串(例如 ref="inputEl"
),却能和 script
中同名的 inputEl
变量绑定在一起?为了解决这些问题,Vue 3.5 推出了 useTemplateRef
函数,使得 ref
的使用更加符合编程直觉。
ref 模版引用的问题
我们先来看一下在 React 中使用 ref
访问 DOM 元素的例子:
const inputEl = useRef<HTMLInputElement>(null);
<input type="text" ref={inputEl} />
useRef
返回的对象是一个 .current
属性,inputEl
通过 ref
属性绑定到 input
元素后,.current
的值就被更新为该 input
元素。这个行为符合编程直觉。
然而,在 Vue 3 中,使用 ref
时的行为却没有这么直观。例如,以下代码中,我们试图像 React 那样在 template
中给 ref
属性绑定一个 ref
变量:
<input type="text" :ref="inputEl" />
const inputEl = ref<HTMLInputElement>();
这么写不但不会报错,但访问 inputEl
时总是 undefined
。原因是 ref
属性接受的并不是 ref
变量,而是其名称。正确的代码应该是这样的:
<input type="text" ref="inputEl" />
const inputEl = ref<HTMLInputElement>();
如果我们将 ref
模版引用的逻辑抽取为 hooks,情况更加复杂。即使 Vue 组件中不使用该变量,我们仍需要在组件中导入该 ref
变量。这看起来很奇怪。
useTemplateRef 函数
为了解决这些问题,Vue 3.5 引入了 useTemplateRef
函数。这个函数的用法非常简单:它接收一个参数 key
(字符串),返回一个 ref
变量。这个 key
值与 template
中 ref
属性的值相同。返回的 ref
变量指向 DOM 元素或子组件。
示例代码如下:
<template>
<input type="text" ref="inputRef" />
<button @click="setInputValue">给 input 赋值</button>
</template>
<script setup lang="ts">
import { useTemplateRef } from "vue";
const inputEl = useTemplateRef<HTMLInputElement>("inputRef");
function setInputValue() {
if (inputEl.value) {
inputEl.value.value = "Hello, world!";
}
}
</script>
这样,inputEl
作为一个 ref
变量,可以通过 useTemplateRef
函数获取到 input
元素。相比之前的做法,这种方式更符合编程直觉。
hooks 中使用 useTemplateRef
对于 hooks 中的例子,我们可以用 useTemplateRef
来简化代码:
export default function useInput(key) {
const inputEl = useTemplateRef<HTMLInputElement>(key);
function setInputValue() {
if (inputEl.value) {
inputEl.value.value = "Hello, world!";
}
}
return {
setInputValue,
};
}
组件中的代码如下:
<template>
<input type="text" ref="inputRef" />
<button @click="setInputValue">给 input 赋值</button>
</template>
<script setup lang="ts">
import useInput from "./useInput";
const { setInputValue } = useInput("inputRef");
</script>
使用 useTemplateRef
后,我们不再需要在组件中导入 inputEl
变量。
动态切换 ref 绑定的变量
有时我们需要根据场景动态切换 ref
绑定的变量,这时 ref
属性的值是动态的。useTemplateRef
同样支持这种场景:
<template>
<input type="text" :ref="refKey" />
<button @click="switchRef">切换 ref 绑定的变量</button>
<button @click="setInputValue">给 input 赋值</button>
</template>
<script setup lang="ts">
import { useTemplateRef, ref } from "vue";
const refKey = ref("inputEl1");
const inputEl1 = useTemplateRef<HTMLInputElement>("inputEl1");
const inputEl2 = useTemplateRef<HTMLInputElement>("inputEl2");
function switchRef() {
refKey.value = refKey.value === "inputEl1" ? "inputEl2" : "inputEl1";
}
function setInputValue() {
const curEl = refKey.value === "inputEl1" ? inputEl1 : inputEl2;
if (curEl.value) {
curEl.value.value = "Hello, world!";
}
}
</script>
通过动态切换 refKey
,可以改变绑定的 ref
变量。
useTemplateRef 的实现
useTemplateRef
的实现实际上很简单:
function useTemplateRef(key) {
const i = getCurrentInstance();
const r = shallowRef(null);
if (i) {
const refs = i.refs === EMPTY_OBJ ? (i.refs = {}) : i.refs;
Object.defineProperty(refs, key, {
enumerable: true,
get: () => r.value,
set: (val) => (r.value = val),
});
}
return r;
}
在初始化时,template
中处理 ref
属性时会对 Vue 实例上的 refs
属性进行写操作,并触发 set
拦截器,将 ref
变量的值更新为 DOM 元素或组件实例。
总结
Vue 3.5 中新增的 useTemplateRef
函数解决了使用 ref
时的一些直觉问题,使得模板中的 ref
属性与 script
中的 ref
变量绑定更加合理。此外,它使得 hooks 中的逻辑更加清晰,减少了不必要的变量导入。通过 useTemplateRef
,我们可以更方便地在模板中获取 DOM 元素或子组件。