文章目录
- 参考
- 描述
- 模板引用
- 引用
- 访问模板引用
- 组件中的模板引用
- $nextTick()
- 示例
- updated
- 错误示范
- 正确演示
- $nextTick()
参考
项目 | 描述 |
---|---|
搜索引擎 | Bing |
哔哩哔哩 | 黑马程序员 |
VueJS 官方文档 | 模板引用 |
描述
项目 | 描述 |
---|---|
Edge | 109.0.1518.70 (正式版本) (64 位) |
操作系统 | Windows 10 专业版 |
@vue/cli | 5.0.8 |
npm | 8.19.3 |
VueJS | 2.6.14 |
模板引用
虽然 Vue 的声明性渲染模型为我们抽象了大部分对 DOM 的直接操作,但在某些情况下,我们仍然需要直接访问底层 DOM 元素。好在,VueJS 提供的模板引解决了这个问题。
引用
当你需要对一个元素进行 DOM 操作时,请先为其添加 ref 属性并设置属性值以标识该元素。
举个栗子
<template>
<div class="container">
<!-- 使用 ref 为该元素添加引用并将该引用标识为 boxRef -->
<div class="box" ref="boxRef"></div>
</div>
</template>
<script>
export default {
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
.box{
width: 100px;
height: 100px;
background-color: dodgerblue;
cursor: pointer;
}
</style>
访问模板引用
我们可以通过 Vue 实例对象的 $refs 来对 ref 引用进行访问。
让我们为 .box 元素添加一个点击事件,在对该元素进行点击后打印 $refs 对象。
<template>
<div class="container">
<!-- 使用 ref 为该元素添加引用并将该引用标识为 boxRef -->
<div class="box" ref="boxRef" v-on:click="convert()"></div>
</div>
</template>
<script>
export default {
// 定义事件调用函数
methods: {
convert() {
console.log(this.$refs);
}
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
.box{
width: 100px;
height: 100px;
background-color: dodgerblue;
cursor: pointer;
}
</style>
执行结果:
在执行上述代码后,点击 .box 元素,你将在控制台中观察到如下信息:
可以看到,在 $refs 对象中,ref 标识作为属性名,而相应元素的 DOM 对象则作为属性值。
因此,我们可以通过如下方式来对 ref 关联的元素进行 DOM 操作。
举个栗子
<template>
<div class="container">
<!-- 使用 ref 为该元素添加引用并将该引用标识为 boxRef -->
<div class="box" ref="boxRef" v-on:click="convert()"></div>
</div>
</template>
<script>
export default {
// 定义事件调用函数
methods: {
convert() {
// 在点击 .box 元素后将该元素的背景颜色将由道奇蓝转变为粉红色
this.$refs.boxRef.style.backgroundColor = 'pink';
}
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
.box{
width: 100px;
height: 100px;
background-color: dodgerblue;
cursor: pointer;
}
</style>
执行效果:
在点击 .box 元素后将该元素的背景颜色将由道奇蓝转变为粉红色。
组件中的模板引用
如果你为组件添加了 ref 属性并设置属性值标识了该引用,那么通过 $refs 你将能够获得该组件对应的 Vue 实例对象,这意味着父组件对子组件的每一个属性和方法都拥有有完全的访问权。这使得在父组件和子组件之间创建紧密耦合的实现细节变得很容易,当然也因此,应该只在绝对需要时才使用组件引用。大多数情况下,你应该首先使用标准的 props 和 emit 接口来实现父子组件交互。
举个栗子
在点击父组件中的 button 元素后,通过模板引用将子组件中的 .box 元素背景颜色进行修改。
App.vue
<template>
<div class="container">
<!-- 为子组件添加 ref 引用并将该引用标识为 Child -->
<Child ref="Child"></Child>
<button @click="transfer()">Transfer</button>
</div>
</template>
<script>
// 导入组件
import Child from '@/components/Child.vue';
export default {
// 注册组件
components: {
Child
},
// 定义事件处理函数
methods: {
transfer() {
// 获取子组件中的 $refs,并通过
// 子组件的 ref 引用对 .box 进行 DOM 操作
this.$refs.Child.$refs.boxRef.style.backgroundColor = 'pink';
}
}
}
</script>
<style>
</style>
Child.vue
<template>
<div class="container">
<!-- 为 .box 元素添加 ref 引用并将该引用标识为 refBox -->
<div class="box" ref="boxRef"></div>
</div>
</template>
<script>
export default {
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
.box{
width: 100px;
height: 100px;
background-color: dodgerblue;
cursor: pointer;
}
</style>
执行效果:
$nextTick()
在讲解 $nextTick() 前,先让我们观察一个示例。
示例
<template>
<div class="container">
<!--
当 v-if 的属性值对应的布尔值为
false 时,该元素将被隐藏
-->
<div class="input" v-if="visible">
<!-- 添加 ref 引用,便于后续该输入框获得焦点 -->
<input type="text" placeholder="请输入您的账号" ref="user">
<input type="password" placeholder="请输入您的密码">
</div>
<!--
当 .input 元素的 v-if 的属性值对应的
布尔值为 false 时,显示 button 元素
-->
<button @click="convert()" v-else>Login</button>
</div>
</template>
<script>
export default {
// 定义数据
data() {
return {
visible: false
}
},
// 定义事件处理函数
methods: {
convert() {
// 在点击 Login 按钮后显示输入框
this.visible = true;
// 在显示输入框后,使第一个输入框获得焦点
this.$refs.user.focus();
}
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
input{
display: block;
margin-top: 15px;
width: 250px;
height: 26px;
}
</style>
预期效果:
在点击 Login 后,显示输入框并隐藏按钮。在此之后,使第一个输入框获得焦点。
执行效果:
可以看到,在点击 Login 后,第一个输入框并未自动获取到焦点。为寻找原因,打开控制台。
在控制台中显示了如下报错信息:
错误信息:
错误信息表明,此时的 this.$refs.user 的值为 undefined,即我们并未获取到 this.$refs.user 对应的 DOM 对象。
分析:
Vue 实例对象(组件)在运行阶段存在生命周期 beforeUpdate 及 updated。在 VueJS 中的响应式状态发生变化时,将进入 updated 生命周期,DOM 节点将在此时期中发生更新。
在上一个示例中,我们修改数据 visible 使输入框得以显示。但在修改数据 visible 时,组件的生命周期还处于 beforeUpdate 生命周期,此时 DOM 节点尚未更新,因此无法通过 this.$refs.user 获取到相关的 DOM 对象。
updated
错误示范
我们可以通过使用 updated 生命周期对应的生命周期钩子,在合适的时机通过 this.$refs.user 操作相关的 DOM 对象来使得元素获取焦点。
该方法是可行的,但在组件中存在多种导致响应式状态发生改变的可能时将存在弊端。为了展现这一弊端,我将数据 secret 与密码输入框中的内容进行了双向绑定,通过 p 元素将数据的变化在页面中加以展示。
<template>
<div class="container">
<!--
当 v-if 的属性值对应的布尔值为
false 时,该元素将被隐藏
-->
<div class="input" v-if="visible">
<!-- 添加 ref 引用,便于后续该输入框获得焦点 -->
<input type="text" placeholder="请输入您的账号" ref="user">
<input type="password" placeholder="请输入您的密码" v-model:value="secret">
<p v-text="secret"></p>
</div>
<!--
当 .input 元素的 v-if 的属性值对应的
布尔值为 false 时,显示 button 元素
-->
<button @click="convert()" v-else>Login</button>
</div>
</template>
<script>
export default {
// 定义数据
data() {
return {
visible: false,
secret: ''
}
},
// 定义事件处理函数
methods: {
convert() {
// 在点击 Login 按钮后显示输入框
this.visible = true;
}
},
// 使用生命周期 updated 对应的生命周期钩子
updated() {
this.$refs.user.focus();
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
input{
display: block;
margin-top: 15px;
width: 250px;
height: 26px;
}
</style>
执行效果:
分析:
密码输入框中的内容与数据达成了双向绑定,密码输入框中的内容变换将导致数据发生变化,组件将因此进入 update 生命周期。
在进入 update 生命周期后,该周期对应的生命周期钩子(生命周期函数)将被执行,于是账号输入框又获取到了焦点。
正确演示
为了解决上一示例中遇到的问题,我们可以为 Login 按钮添加 “状态”,在进入生命周期函数后,我们将对 Login 的状态进行判断来决定是否需要执行 this.$refs.user.focus(); 来使账号输入框获取焦点。
<template>
<div class="container">
<!--
当 v-if 的属性值对应的布尔值为
false 时,该元素将被隐藏
-->
<div class="input" v-if="visible">
<!-- 添加 ref 引用,便于后续该输入框获得焦点 -->
<input type="text" placeholder="请输入您的账号" ref="user">
<input type="password" placeholder="请输入您的密码" v-model:value="secret">
<p v-text="secret"></p>
</div>
<!--
当 .input 元素的 v-if 的属性值对应的
布尔值为 false 时,显示 button 元素
-->
<button @click="convert()" v-else>Login</button>
</div>
</template>
<script>
export default {
// 定义数据
data() {
return {
visible: false,
secret: '',
// 定义数据 flag 用于标识 Login 的点击状态
flag: false
}
},
// 定义事件处理函数
methods: {
convert() {
// 在点击 Login 按钮后显示输入框
this.visible = true;
// 在点击 Login 按钮后修改数据 flag
this.flag = true;
}
},
// 使用生命周期 updated 对应的生命周期钩子
updated() {
// 判断 Login 按钮的点击状态
if(!this.flag){
this.$refs.user.focus();
}
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
input{
display: block;
margin-top: 15px;
width: 250px;
height: 26px;
}
</style>
执行效果:
$nextTick()
当你在 Vue 中更改响应式状态时,最终的 DOM 更新并不是同步生效的,而是由 Vue 将它们缓存在一个队列中,直到下一个 tick 才一起执行。这样是为了确保每个组件无论发生多少状态改变,都仅执行一次更新。
$nextTick() 可以在响应式状态发生改变后立即使用,你可以传递一个回调函数或者 await 返回的 Promise 作为 $nextTick() 的参数。$nextTick() 函数会在 DOM 更新完成后执行参数所关联的代码块。
<template>
<div class="container">
<!--
当 v-if 的属性值对应的布尔值为
false 时,该元素将被隐藏
-->
<div class="input" v-if="visible">
<!-- 添加 ref 引用,便于后续该输入框获得焦点 -->
<input type="text" placeholder="请输入您的账号" ref="user">
<input type="password" placeholder="请输入您的密码">
</div>
<!--
当 .input 元素的 v-if 的属性值对应的
布尔值为 false 时,显示 button 元素
-->
<button @click="convert()" v-else>Login</button>
</div>
</template>
<script>
export default {
// 定义数据
data() {
return {
visible: false
}
},
// 定义事件处理函数
methods: {
convert() {
// 在点击 Login 按钮后显示输入框
this.visible = true;
// 在 DOM 更新后使账号输入框获得焦点
this.$nextTick(() => {
this.$refs.user.focus();
})
}
}
}
</script>
// 为 style 元素添加 scoped 属性
// 防止出现样式冲突
<style scoped>
input{
display: block;
margin-top: 15px;
width: 250px;
height: 26px;
}
</style>
执行效果: