一幅长文细学Vue(六)——组件高级(下)
6 组件高级(下)
摘要:在本文中我们会谈及ref引用DOM和组件实例、并且了解$nextTick的调用时机,进而谈论keep-alive元素的作用、插槽的基本用法和自定义指令。
声明:为了文章的清爽性,在文章内部的代码演示中只会附上部分演示代码,main.js文件的代码通常不贴出,如果感兴趣可以前往代码仓库获取
作者:来自ArimaMisaki创作
6.1 ref
6.1.1 了解ref引用
说明:ref用来辅助开发者在不依赖原生的js和JQuery的情况下,获取DOM元素或组件的引用。
提示:在每个vue的组件实例中,都包含一个$refs对象
,里面存储这对应的DOM元素或组件的引用。默认情况下,组件的$refs指向一个空对象。
检验:我们可以通过组件的this来调用当前组件的实例对象,从而查看里面是否存在$refs。
<template>
<div>
<h1>App根组件</h1>
<hr>
<button type="button" @click="getRefs">获取$ref引用</button>
</div>
</template>
<script>
export default {
name:"MyApp",
methods:{
getRefs(){
console.log(this);
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
6.1.2 使用ref引用DOM元素
说明:在第二章我们曾经介绍过,使用this.$refs.ref
属性以及在DOM中定义ref属性即可获取DOM元素。
<template>
<div>
<h1 ref="rootApp">App根组件</h1>
<hr>
<button type="button" @click="getRefs">获取$ref引用</button>
<button type="button" @click="changeColor">改变标题颜色</button>
</div>
</template>
<script>
export default {
name:"MyApp",
methods:{
getRefs(){
console.log(this);
},
changeColor(){
this.$refs.rootApp.style.color = 'red';
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
6.1.3 使用ref引用组件实例
说明:通过在组件标签上设置ref属性可以让我们获得操纵组件的能力。
<template>
<div>
<my-counter ref="counterRef"></my-counter>
<button type="type" @click="resetRef">重置counter</button>
</div>
</template>
<script>
import MyCounter from './Counter.vue';
export default {
name:"MyApp",
methods:{
resetRef(){
this.$refs.counterRef.reset();
}
},
components:{
MyCounter
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
<template>
<div>
<p>{{ count }}</p>
<button type="button" @click="add">+1</button>
</div>
</template>
<script>
export default {
name: 'MyCounter',
data() {
return {
count: 0
}
},
methods: {
add() {
this.count++;
},
// 一个重置函数供App组件的ref引用
reset(){
this.count = 0;
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
6.1.4 ref应用场景
说明:ref常用语文本框获取焦点。具体实现步骤是通过ref获取DOM元素,并调用DOM元素原生的focus方法。
提示:在下面给出的代码中,你会发现实际上做不到想要的效果。打印iptRef居然无法获得DOM,这是因为对于组件来说DOM的更新是异步执行
的。
<template>
<div>
<h1>App根组件</h1>
<hr>
<input type="text" v-if="inputVisible" ref="iptRef">
<button type="button" v-else @click="showInput">展示input输入框</button>
</div>
</template>
<script>
export default {
name:'MyApp',
data(){
return{
inputVisible:false
}
},
methods:{
showInput(){
// 展示文本框
this.inputVisible = true;
// 通过ref获取文本框并且调用focus自动获取焦点
this.$refs.iptRef.focus();
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
6.1.5 $nextTick函数的作用
响应式状态:通过data改变DOM中数据,或者是DOM更新。
tick更新:我们前面谈论过组件更新,当时说DOM的更新是异步执行
的,这是因为Vue将更新的DOM元素缓存在一个队列中,直到下一个tick才一起执行,这样可以防止修改一个DOM元素立马响应,从而提高更新速度。
说明:如果我们想要在状态改变后使用更新后的DOM,则使用nextTick()方法可以让我们执行的动作延迟到DOM更新结束,即下一个tick。nextTick()接收一个回调函数作为参数,或者await返回的Promise。
<template>
<div>
<h1>App根组件</h1>
<hr>
<input type="text" v-if="inputVisible" ref="iptRef">
<button type="button" v-else @click="showInput">展示input输入框</button>
</div>
</template>
<script>
export default {
name:'MyApp',
data(){
return{
inputVisible:false
}
},
methods:{
showInput(){
// 展示文本框
this.inputVisible = true;
// 通过ref获取文本框并且调用focus自动获取焦点
this.$nextTick(()=>{
this.$refs.iptRef.focus();
})
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
6.2 动态组件
6.2.1 什么是动态组件
说明:动态组件指的是动态切换组件的显示和隐藏。Vue提供了一个内置的<component>组件,专门用来实现组件的动态渲染。
6.2.2 实现动态组件渲染
元素:在下面的诸多术语中,我们会把标签说成元素
。
实现步骤:
- 用component
元素
对原来的动态组件标签进行替换,相当于一个占位标签 - 在data节点中声明默认显示的组件
- 在component标签中采用
:is
的方式绑定data中的组件名
<template>
<div>
<p>Home组件</p>
</div>
</template>
<script>
export default {
name:'MyHome'
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
<template>
<div>
<p>Movie组件</p>
</div>
</template>
<script>
export default {
name:'MyMovie'
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
<template>
<div>
<h1>App根组件</h1>
<hr>
<button @click="this.comName = 'MyHome'">首页</button>
<button @click="this.comName = 'MyMovie'">电影</button>
<!-- <my-home></my-home>
<my-movie></my-movie> -->
<!-- 1.用component标签对原来的动态组件标签进行替换,相当于一个占位标签 -->
<!-- 3.在component标签中采用`:is`的方式绑定data中的组件名 -->
<component :is="comName"></component>
</div>
</template>
<script>
import MyHome from "./Home.vue";
import MyMovie from "./Movie.vue"
export default {
name:'MyApp',
data(){
return {
// 2.在data节点中声明默认显示的组件
comName:'MyHome',
}
},
components:{
MyHome,
MyMovie
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
6.2.3 保持组件状态
引入:如果组件中包含有数据的变动,那么在切换组件时,被切换的组件中数据会被恢复为最原始的部分。我们来描述这么一个场景,A1组件有count值,默认值为1,但被我通过方法增加到了5,如果此时切换为A2组件,切回A1时我们会发现count又变回1了。
说明:组件状态不能在组件切换中保持,本质原因是因为组件在切换的时候从内存中卸载,这个事实我们可以使用生命周期钩子验证。如果想要保持其存活性,我们可以在component标签
的外围用keep-alive标签
包裹,使得被卸载的组件仍然存活。
<template>
<div>
<h1>App根组件</h1>
<hr>
<button @click="this.comName = 'MyHome'">首页</button>
<button @click="this.comName = 'MyMovie'">电影</button>
<!-- 使用keep-alive包裹 -->
<keep-alive><component :is="comName"></component></keep-alive>
</div>
</template>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
<template>
<div>
<p>Home组件---{{count}}</p>
<button @click="this.count++">+1</button>
</div>
</template>
<script>
export default {
name:'MyHome',
data(){
return{
count:0
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
6.3 插槽
6.3.1 什么是插槽
说明:插槽(Slot)是vue为组件的封装者
提供的能力,它允许组件的开发人员预留一片空白的区域,这片区域啥也没有,里面可以有什么样的东西由组件的使用者自行决定。
6.3.2 插槽基本使用
说明:在封装组件时,通过slot元素可以定义插槽,从而为组件的使用者预留占位。
提示:如果在没有使用slot元素定义插槽的情况下直接在组件标签中添加自定义内容,那添加的内容会全部被丢弃。
后备内容:在封装组件时,可以为预留的slot插槽提供默认内容,一旦组件使用者没有为插槽插入内容,则启用默认的内容。具体做法是在slot元素中写入默认的内容。
<template>
<div>
<p>插槽在下面</p>
<slot>默认内容</slot>
<p>插槽在上面</p>
</div>
</template>
<script>
export default {
name:'MyCom'
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
<template>
<div>
<h1>App根组件</h1>
<hr>
<!-- 使用带有插槽的组件 -->
<my-com>
<!-- 里面具有内容会被插入插槽中 -->
<p>插槽在这里!</p>
</my-com>
</div>
</template>
<script>
// 导入组件
import MyCom from './MyCom.vue'
export default {
name:'MyApp',
components:{
MyCom
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
6.3.3 具名插槽
说明:如果在封装组件时需要拥有多个插槽,则需要为每个slot指定具体的name名称。我们把带有具体名称的插槽叫做具名插槽
。
提示:如果一个slot没有指定name名称,则其默认名称为default
。
使用步骤:
- 在多个插槽上添加name属性
- 在组件的使用者中使用带有v-slot指令的template元素包裹将要插入插槽的内容,在template元素中加入
v-slot:插槽名
v-slot:插槽名
可以简写为#插槽名
<template>
<div>
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
</template>
<script>
export default {
name:'MyArticle'
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
<template>
<div>
<h1>App根组件</h1>
<my-artcle>
<template #header>
<h1>滕王阁序</h1>
</template>
<p>豫章故郡,洪都新府</p>
<p>星分翼轸,地接衡庐</p>
<template #footer>
<p>落款:王勃</p>
</template>
</my-artcle>
</div>
</template>
<script>
import MyArtcle from "./MyArticle.vue"
export default {
name:'MyApp',
components:{
MyArtcle,
},
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
6.3.4 渲染作用域
说明:插槽内容可以访问到组件使用者的数据作用域,因为插槽内容本身是在组件使用者的模板中定义的。换而言之,假设有一个组件使用者的data选项中有一个数据count,那么插槽可以获取到这个数据。
提示:插槽内容虽然可以访问组件使用者的数据,却无法访问拥有插槽的组件数据。
<template>
<div>
<h1>App根组件</h1>
<my-artcle>
<template #header>
<!-- 插槽可以访问组件使用者的数据 -->
<h1>{{title}}</h1>
</template>
<p>豫章故郡,洪都新府</p>
<p>星分翼轸,地接衡庐</p>
<template #footer>
<p>落款:王勃</p>
</template>
</my-artcle>
</div>
</template>
<script>
import MyArtcle from "./MyArticle.vue"
export default {
name:'MyApp',
components:{
MyArtcle,
},
data(){
return {
title:'滕王阁序',
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
6.3.5 作用域插槽
说明:在上一小节提到过插槽内容只可以访问组件使用者的数据,却无法访问拥有插槽的组件数据。为此,我们提出了作用域插槽
。作用域插槽能够同时访问到组件使用者以及拥有插槽的组件两者的数据。
具体的做法如同对组件传递props一般,我们可以向一个插槽的出口上传递属性。
使用步骤:
- 在组件预留插槽时,在slot元素上通过v-bind绑定data的属性,绑定的方式与以往略有不同,详见下面演示的代码。
- 在组件的使用者上,找到组件标签,为其
v-slot:插槽名
指定接收值,接收值为任意的名字。 - 当我们需要使用带有插槽组件的数据时,我们只需要通过接收值指定的名字来访问对象,该对象中带有v-bind绑定的所有数据名,通过它我们可以访问到数据。
提示:虽然和props很像,但我们并不用声明props选项,如下图所示。
<template>
<div>
<h3>TEST组件</h3>
<slot name="default" :info="infomation"></slot>
</div>
</template>
<script>
export default {
name:'MyTest',
data(){
return {
infomation:{
phone:'138xxxx6666',
address:'中国北京',
}
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
<template>
<div>
<h1>App根组件</h1>
<my-test>
<template #default="scope">
<p>{{scope.info.address}}</p>
</template>
</my-test>
</div>
</template>
<script>
import MyTest from "./MyTest.vue"
export default {
name:"MyApp",
components:{
MyTest,
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
6.3.6 解构作用域插槽的prop
说明:在ES6中我们曾经提到编程风范。如果想要提取对象中的某个属性,最好的方式是解构赋值。
<my-test>
<template #default="{info}">
<p>{{info.address}}</p>
</template>
</my-test>
- 1
- 2
- 3
- 4
- 5
6.4 自定义指令
6.4.1 什么是自定义指令
说明:vue官方提供了v-for、v-model等诸多内置指令,除此之外,Vue还允许开发者自定义指令。vue的自定义指令分为私有自定义指令
和全局自定义指令
。私有自定义指令仅在定义指令的组件内可用;而全局自定义指令可在项目中任意位置使用。
作用:自定义指令主要是为了重用涉及普通元素的底层DOM访问的逻辑。
提示:自定义指令在使用时必须以v-
开头,但在声明时却无需加上v-
。还有,只有当所需功能只能通过直接的 DOM 操作来实现时,才应该使用自定义指令。其他情况下应该尽可能地使用 v-bind
这样的内置指令来声明式地使用模板,这样更高效,也对服务端渲染更友好。
6.4.2 创建私有自定义指令
说明:在directives选项中可以自定义指令。下面以自定义指令v-focus来实现文本框自动聚焦为例。
使用步骤:
- 在directives选项中声明focus对象,
- 在focus对象中使用生命周期钩子,这里使用mounted函数,使得自定义指令在组件被渲染完成时生效。
- 为mounted传入el参数,el表示使用了v-focus指令的标签。
<template>
<div class="home-container">
<h3>MyHome 组件</h3>
<hr>
<input type="text" class="form-control" v-focus>
</div>
</template>
<script>
export default {
name:'MyHome',
directives:{
focus:{
mounted(el){
el.focus();
}
}
}
}
</script>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
6.4.3 创建全局自定义指令
说明:与注册组件类似,在directives选项中创建的指令仅仅是局部的。如果想要声明一个全局的自定义指令,我们需要在main.js中使用app.directive()
来声明。
const app = createApp(App);
app.directive('focus',{
mounted(el){
el.focus()
}
})
- 1
- 2
- 3
- 4
- 5
- 6
- 7
6.4.4 指令钩子
说明:一个指令对象可以提供多种可选的生命周期钩子。
钩子参数:
el
:指令绑定到的元素。这可以用于直接操作 DOM。binding
:一个对象,包含以下属性。value
:传递给指令的值。例如在v-my-directive="1 + 1"
中,值是2
。oldValue
:之前的值,仅在beforeUpdate
和updated
中可用。无论值是否更改,它都可用。arg
:传递给指令的参数 (如果有的话)。例如在v-my-directive:foo
中,参数是"foo"
。modifiers
:一个包含修饰符的对象 (如果有的话)。例如在v-my-directive.foo.bar
中,修饰符对象是{ foo: true, bar: true }
。instance
:使用该指令的组件实例。dir
:指令的定义对象。
vnode
:代表绑定元素的底层 VNode。prevNode
:之前的渲染中代表指令所绑定元素的 VNode。仅在beforeUpdate
和updated
钩子中可用。
const myDirective = {
// 在绑定元素的 attribute 前
// 或事件监听器应用前调用
created(el, binding, vnode, prevVnode) {
// 下面会介绍各个参数的细节
},
// 在元素被插入到 DOM 前调用
beforeMount(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都挂载完成后调用
mounted(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件更新前调用
beforeUpdate(el, binding, vnode, prevVnode) {},
// 在绑定元素的父组件
// 及他自己的所有子节点都更新后调用
updated(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载前调用
beforeUnmount(el, binding, vnode, prevVnode) {},
// 绑定元素的父组件卸载后调用
unmounted(el, binding, vnode, prevVnode) {}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
6.4.5 简化写法
说明:对于自定义指令来说,很多钩子是不经常使用的。经常使用的钩子使用mounted和updated。为此,我们可以直接定义一个函数来定义指令,而无需再传对象。
app.directive('color', (el, binding) => {
// 这会在 `mounted` 和 `updated` 时都调用
el.style.color = binding.value
})
- 1
- 2
- 3
- 4
文章来源: blog.csdn.net,作者:ArimaMisaki,版权归原作者所有,如需转载,请联系作者。
原文链接:blog.csdn.net/chengyuhaomei520/article/details/126605565
- 点赞
- 收藏
- 关注作者
评论(0)