Vue3.5新增useTemplateRef让ref操作DOM更加简单

目录
文章目录隐藏
  1. ref 模版引用的问题
  2. useTemplateRef 函数
  3. hooks 中使用 useTemplateRef
  4. 动态切换 ref 绑定的变量
  5. useTemplateRef 是如何实现的?
  6. 总结

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 元素或者组件实例。

「点点赞赏,手留余香」

1

给作者打赏,鼓励TA抓紧创作!

微信微信 支付宝支付宝

还没有人赞赏,快来当第一个赞赏的人吧!

声明:本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系maynote@foxmail.com处理
码云笔记 » Vue3.5新增useTemplateRef让ref操作DOM更加简单

发表回复