Vue3.5新增useTemplateRef让ref操作DOM更加简单
vue3 中想要访问 DOM 和子组件可以使用 ref 进行模版引用,但是这个 ref 有一些让人迷惑的地方。比如定义的 ref 变量到底是一个响应式数据还是 DOM 元素?还有 template 中 ref 属性的值明明是一个字符串,比如ref="inputEl"
,怎么就和 script 中同名的inputEl
变量绑到一块了呢?所以 Vue3.5 推出了一个useTemplateRef
函数,完美的解决了这些问题。
ref 模版引用的问题
我们先来看一个react
中使用 ref 访问 DOM 元素的例子,代码如下:
const inputEl = useRef<HTMLInputElement>(null); <input type="text" ref={inputEl} />
使用 useRef 函数定义了一个名为 inputEl 的变量,然后将 input 元素的 ref 属性值设置为 inputEl 变量,这样就可以通过 inputEl 变量访问到 input 输入框了。
inputEl 因为是一个.current
属性的对象,由于 inputEl 变量赋值给了 ref 属性,所以他的.current
属性的值被更新为了 input DOM 元素,这个做法很符合编程直觉。 再来看看 vue3 中的做法,相比之下就很不符合编程直觉了。 不知道有多少同学最开始接触 vue3 时总是在 template 中像 react 一样给 ref 属性绑定一个 ref 变量,而不是 ref 变量的名称。比如下面这样的代码:
<input type="text" :ref="inputEl" /> const inputEl = ref<HTMLInputElement>();
更加要命的是这样写还不会报错!!!!当我们使用inputEl
变量去访问 input 输入框时始终拿到的都是undefined
。
经过多次排查发现原来 ref 属性接收的不是一个 ref 变量,而是 ref 变量的名称。正确的代码应该是这样的:
<input type="text" ref="inputEl" /> const inputEl = ref<HTMLInputElement>();
还有就是如果我们将 ref 模版引用相关的逻辑抽成 hooks 后,那么必须将在 vue 组件中也要将 ref 属性对应的 ref 变量也定义才可以。
hooks 代码如下:
export default function useRef() { const inputEl = ref<HTMLInputElement>(); function setInputValue() { if (inputEl.value) { inputEl.value.value = "Hello, world!"; } } return { inputEl, setInputValue, }; }
在 hooks 中定义了一个名为inputRef
的变量,并且在setInputValue
函数中会通过inputRef
变量对 input 输入框进行操作。
vue 组件代码如下:
<template> <input type="text" ref="inputEl" /> <button @click="setInputValue">给 input 赋值</button> </template> <script setup lang="ts"> import useInput from "./useInput"; const { setInputValue, inputEl } = useInput(); </script>
虽然在 vue 组件中我们不会使用inputEl
变量,但是还是需要从 hooks 中导入useInput
变量。大家不觉得这很奇怪吗?导入了一个变量,又没有显式的去使用这个变量。
如果在这里不去从 hooks 中导入inputEl
变量,那么inputEl
变量中就不能绑定上 input 输入框了。
useTemplateRef 函数
为了解决上面说的 ref 模版引用的问题,在 Vue3.5 中新增了一个useTemplateRef
函数。
useTemplateRef
函数的用法很简单:只接收一个参数key
,是一个字符串。返回值是一个 ref 变量。
其中参数 key 字符串的值应该等于 template 中 ref 属性的值。
返回值是一个 ref 变量,变量的值指向模版引用的 DOM 元素或者子组件。
我们来看个例子,前面的 demo 改成useTemplateRef
函数后代码如下:
<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>
在 template 中 ref 属性的值为字符串"inputRef"
。
在 script 中使用useTemplateRef
函数,传入的第一个参数也是字符串"inputRef"
。useTemplateRef
函数的返回值就是指向 input 输入框的 ref 变量。
由于inputEl
是一个 ref 变量,所以在 click 事件中想要访问到 DOM 元素 input 输入框就需要使用inputEl.value
。
我们这里是要给输入框中塞一个字符串”Hello, world!”,所以使用inputEl.value.value = "Hello, world!"
使用了useTemplateRef
函数后和之前比起来就很符合编程直觉了。template 中 ref 属性值是一个字符串"inputRef"
,使用useTemplateRef
函数时也传入字符串"inputRef"
就能拿到对应的模版引用了。
hooks 中使用 useTemplateRef
回到前面讲的 hooks 的例子,使用useTemplateRef
后 hooks 代码如下:
export default function useInput(key) { const inputEl = useTemplateRef<HTMLInputElement>(key); function setInputValue() { if (inputEl.value) { inputEl.value.value = "Hello, world!"; } } return { setInputValue, }; }
现在我们在 hooks 中就不需要导出变量inputEl
了,因为这个变量只需要在 hooks 内部使用。
vue 组件代码如下:
<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>
由于在 vue 组件中我们不需要使用inputEl
变量,所以在这里就不需要从useInput
中引入变量inputEl
了。而之前不使用useTemplateRef
的方案中我们就不得不引入inputEl
变量了。
动态切换 ref 绑定的变量
有的时候我们需要根据不同的场景去动态切换 ref 模版引用的变量,这时在 template 中 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>
在这个场景 template 中 ref 绑定的就是一个变量refKey
,通过点击切换 ref 绑定的变量
按钮可以切换refKey
的值。相应的,绑定 input 输入框的变量也会从inputEl1
变量切换成inputEl2
变量。
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; }
首先使用getCurrentInstance
方法获取当前 vue 实例对象,赋值给变量i
。
然后调用shallowRef
函数生成一个浅层的 ref 对象,初始值为 null。这个 ref 对象就是useTemplateRef
函数返回的 ref 对象。
接着就是判断当前 vue 实例如果存在就读取实例上面的refs
属性对象,如果实例对象上面没有refs
属性,那么就初始化一个空对象到 vue 实例对象的refs
属性。
vue 实例对象上面的这个refs
属性对象用过 vue2 的同学应该都很熟悉,里面存的是注册过 ref 属性的所有 DOM 元素和组件实例。
vue3 虽然不像 vue2 一样将refs
属性对象开放给开发者,但是他的内部依然还是用 vue 实例上面的refs
属性对象来存储 template 中使用 ref 属性注册过的元素和组件实例。
这里使用了Object.defineProperty
方法对refs
属性对象进行拦截,拦截的字段是变量key
的值,而这个key
的值就是 template 中使用 ref 属性绑定的值。
以我们上面的 demo 举例,在 template 中的代码如下:
<input type="text" ref="inputRef" />
这里使用 ref 属性在 vue 实例的refs
属性对象上面注册了一个 input 输入框,refs.inputRef
的值就是指向 DOM 元素 input 输入框。
然后在 script 中是这样使用useTemplateRef
的:
const inputEl = useTemplateRef<HTMLInputElement>("inputRef")
调用useTemplateRef
函数时传入的是字符串"inputRef"
,在useTemplateRef
函数内部使用Object.defineProperty
方法对refs
属性对象进行拦截,拦截的字段为变量key
的值,也就是调用useTemplateRef
函数传入的字符串"inputRef"
。
初始化时,vue 处理 input 输入框上面的ref="inputRef"
就会执行下面这样的代码:
refs[ref] = value
此时的value
的值就是指向 DOM 元素 input 输入框,ref
的值就是字符串"inputRef"
。
那么这行代码就是将 DOM 元素 input 输入框赋值给refs
对象上面的inputRef
属性上。
由于这里对refs
对象上面的inputRef
属性进行写操作,所以会走到useTemplateRef
函数中Object.defineProperty
定义的set
拦截。
代码如下:
const r = shallowRef(null); Object.defineProperty(refs, key, { enumerable: true, get: () => r.value, set: (val) => (r.value = val), });
在set
拦截中会将 DOM 元素 input 输入框赋值给 ref 变量r
,而这个r
就是useTemplateRef
函数返回的 ref 变量。
同样的当对象refs
对象的inputRef
属性进行读操作时,也会走到这里的get
拦截中,返回useTemplateRef
函数中定义的 ref 变量r
的值。
总结
Vue3.5 中新增的useTemplateRef
函数解决了 ref 属性中存在的几个问题:
- 不符合编程直觉,template 中 ref 属性的值是 script 中对应的 ref 变量的变量名。
- 在 script 中如果不使用 ts,则不能直观的知道一个 ref 变量到底是响应式数据还是 DOM 元素?
- 将定义和访问 DOM 元素相关的逻辑抽到 hooks 中后,虽然 vue 组件中不会使用到存放 DOM 元素的变量,但是也必须在组件中从 hooks 中导入。
接着我们讲了useTemplateRef
函数的实现。在useTemplateRef
函数中会定义一个 ref 对象,在useTemplateRef
函数最后就是 return 返回这个 ref 对象。
接着使用Object.defineProperty
对 vue 实例上面的refs
属性对象进行 get 和 set 拦截。
初始化时,处理 template 中的 ref 属性,会对 vue 实例上面的refs
属性对象进行写操作。
然后就会被 set 拦截,在 set 拦截中会将useTemplateRef
函数中定义的 ref 对象的值赋值为绑定的 DOM 元素或者组件实例。
而useTemplateRef
函数就是将这个 ref 对象进行 return 返回,所以我们可以通过useTemplateRef
函数的返回值拿到 template 中 ref 属性绑定的 DOM 元素或者组件实例。
码云笔记 » Vue3.5新增useTemplateRef让ref操作DOM更加简单