前言

vue3中想要访问DOM和子组件可以使用ref进行模版引用,但是这个ref有一些让人迷惑的地方。比如定义的ref变量到底是一个响应式数据还是DOM元素?还有template中ref属性的值明明是一个字符串,比如ref="inputEl",怎么就和script中同名的inputEl变量绑到一块了呢?所以Vue3.5推出了一个useTemplateRef函数,完美的解决了这些问题。

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

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

关注公众号:【前端欧阳】,给自己一个进阶vue的机会

另外欧阳写了一本开源电子书vue3编译原理揭秘,看完这本书可以让你对vue编译的认知有质的提升。这本书初、中级前端能看懂,完全免费,只求一个star。

牛逼!Vue3.5的useTemplateRef让ref操作DOM更加丝滑的更多相关文章

  1. 【React自制全家桶】三、React使用ref操作DOM与setState遇到的问题

    在React中同时使用ref操作DOM与setState常常会遇到 比如操作的DOM是setState更新之前的DOM内容,与想要的操作不一致.导致这样的原因是setState函数是异步函数. 就是当 ...

  2. 在-for 循环里面如何利用ref 操作dom

    由于dom 元素是在渲染之后才能操作,所以如果想取到dom元素,要放到mounted()这个生命周期函数里面,并且还要用this.$nextTick(function () {})

  3. 为什么我会认为SAP是世界上最好用最牛逼的ERP系统,没有之一?

    为什么我认为SAP是世界上最好用最牛逼的ERP系统,没有之一?玩过QAD.Tiptop.用友等产品,深深觉得SAP是贵的有道理! 一套好的ERP系统,不仅能够最大程度承接适配企业的管理和业务流程,在技 ...

  4. 我喜欢ASP.NET的MVC因为它牛逼的9大理由(转载)

    我很早就关注ASP.NET的mvc的,因为最开始是学了Java的MVC,由于工作的原因一直在做.Net开发,最近的几个新项目我采用了MVC做了,我个一直都非常喜欢.Net的MVC.我们为什么使用MVC ...

  5. 最牛逼android上的图表库MpChart(三) 条形图

    最牛逼android上的图表库MpChart三 条形图 BarChart条形图介绍 BarChart条形图实例 BarChart效果 最牛逼android上的图表库MpChart(三) 条形图 最近工 ...

  6. 最牛逼android上的图表库MpChart(二) 折线图

    最牛逼android上的图表库MpChart二 折线图 MpChart折线图介绍 MpChart折线图实例 MpChart效果 最牛逼android上的图表库MpChart(二) 折线图 最近工作中, ...

  7. 最牛逼android上的图表库MpChart(一) 介绍篇

    最牛逼android上的图表库MpChart一 介绍篇 MpChart优点 MpChart是什么 MpChart支持哪些图表 MpChart效果如何 最牛逼android上的图表库MpChart(一) ...

  8. .Net免费公开课视频+资料+源码+经典牛逼 汇总篇【持续更新】

    博主推荐一:WP8.1最经典培训教程 博主点评:经典Windows Phone8.1 Runtime API培训最经典教程,此教程由传智播客蒋坤老师录制的一整套WP8.1入门级视频教程,讲授内容非常广 ...

  9. 科学家有了钱以后,真是挺吓人的——D.E.Shaw的牛逼人生

    科学家有了钱以后,真是挺吓人的——D.E.Shaw的牛逼人生 黑科技,还是要提D.E.Shaw Research这个奇异的存在. 要讲这个黑科技,我们可能要扯远一点,先讲讲D.E. Shaw这个人是怎 ...

  10. cssViewer牛逼的chrome插件

    很牛逼,功能很强大.

随机推荐

  1. 日常工作中需要避免的9个React坏习惯

    前言 React是前端开发领域中最受欢迎的JavaScript库之一,但有时候在编写React应用程序时,可能陷入一些不佳的习惯和错误做法.这些不佳的习惯可能导致性能下降.代码难以维护,以及其他问题. ...

  2. 推荐一枚宝藏Up主,顺便聊聊感想

    众所周知,B站是学习网站 最近发现一宝藏Up主,主要做科普,主题包括但不限于:大模型的底层算法.量子计算底层原理和硬件设计,以及其他物理或者自然科学主题,总体偏向于理工科. 值得推荐的理由:Up主对底 ...

  3. tp5命名规范

    tp5中对类,文件名,函数和方法的命名规范如下: 类名和类文件名保持一致,并统一采用驼峰法命名(首字母大写) 类的命名采用驼峰法,并且首字母大写,例如 User.UserType,不需要添加contr ...

  4. tp5 为什么使用单例模式

    首先我们要知道明确单例模式这个概念,那么什么是单例模式呢?单例模式顾名思义,就是只有一个实例.作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例, 这个类我们 ...

  5. 玄机-第二章日志分析-mysql应急响应

    目录 前言 简介 应急开始 准备工作 日志分析 步骤 1 步骤 2 步骤 3 步骤 4 总结 补充mysql中的/var/log/mysql/erro.log 记录上传文件信息的原因 前言 这里应急需 ...

  6. java中一些空判断|ObjectUtils

    为什么用ObjectUtils? 在java中判断对象是否为null,常常不止判断对象是否为null,如果对象是集合,数组,字符串等等特殊类型,还需要检查是否为空(元素个数为0或者长度为0)Objec ...

  7. 备份服务器eBackup

    目录 软件包方式安装eBackup备份软件   1.前景提要   2.创建虚拟机   3.安装备份软件.   4.安装 eBackup 补丁   5.配置 eBackup 服务器   6.访问web界 ...

  8. 【Java / JavaScript】AES加密解密

    Java封装的AES加密解密工具类: 几个重要的参数信息 - 需要指定一个密钥串sKey 密钥内容自定义 数字 + 字母 + 特殊符号 - 加密方式为 AES - AES下面的模式ECB - ECB下 ...

  9. 【REGX】正则表达式 选中空白行

    参考地址: https://www.cnblogs.com/peijyStudy/p/13201576.html VScode并列替换不够智能,我需要等行粘贴,结果SHIFT+ALT复制内容粘贴上去就 ...

  10. 中国特供阉割版4090D建议安装最新驱动,据说不然的话会报error:4090和4090D对比

    资料来源: https://www.bilibili.com/video/BV1oa4y127fG/?spm_id_from=333.999.0.0&vd_source=f1d0f27367a ...