TOC
插槽
在开发中,我们会经常复用一个一个可复用的组件,同时,我们也可以通过props或者$emit使组件间实现数据传递,让组件根据不同的数据展示不同的内容,但是这里有一个问题,就是组件就是一套模版,它里面的解构基本都是写死的、固定的;
比如,某些情况我们希望子组件只给我们显示一个<button>元素, 但又因为其他原因,希望组件只显示一个<img>元素,虽然这种需求我们可以通过现有的知识去实现,但是可能比较鸡肋,那么在这种场景下,Vue框架其实提供了一种更好解决方案,让调用者(父组件)来决定,组件显示什么内容,那么在Vue里面,实现这种技术的,就称之为插槽;
插槽(slot)是vue为组件的封装者提供的可编程的能力,允许开发者在封装组件时,把不确定的、希望由父组件指定的部分定义为插槽,从更加简单的方式来理解,其实我们可以把插槽认为是组件封装期间,为父组件预留的占位符,如下图;
插槽基本使用
插槽的使用方式是比较简单的,Vue框架将<slot>元素作为承载分发内容的出口,即上述的占位符,在组件的封装中,可以使用<slot>元素,开启一个插槽,该插槽具体插入什么值,由父组件来决定;
<!-- 父组件 -->
<template>
<div>
<showinfo>插槽</showinfo>
</div>
</template>
<script>
import showinfo from "./components/showinfo.vue"
export default {
components: {
showinfo: showinfo
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button><slot></slot></button>
</div>
</template>
插槽默认值
插槽是可以有默认值的,当父组件引用子组件没有给定插槽的内容时,会直接展示默认值,当给定了内容时,默认值将不会生效,那么想要实现默认值也非常的简单,以上面的代码举例,直接在子组件的<slot>元素内添加默认值的内容即可,如下示例;
<!-- 父组件 -->
<template>
<div>
<showinfo></showinfo>
</div>
</template>
<script>
import showinfo from "./components/showinfo.vue"
export default {
components: {
showinfo: showinfo
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button><slot>默认值</slot></button>
</div>
</template>
命名插槽
当一个子组件内,定义了多个插槽时,就需要用到命名插槽,也称之为具名插槽,命名插槽说白了,就是对插槽取一个名称,然后在父组件准备给插槽插入内容时,指定需要将内容插入到名称为哪一个到插槽中;
那么命名插槽具体的使用,就是在子组件中,给<slot>元素加上一个属性,即name,指定这个插槽的名称,然后再在父组件中,使用v-slot指令来指定需要给哪个名称的插槽插入内容,那么想要使用"v-slot:"指令,还必须使用template属性,这是框架的要求,如下示例;
<!-- 父组件 -->
<template>
<div>
<showinfo>
<template v-slot:add>加法</template> <!-- 使用name属性,指定需要插入slot的名称 -->
<template v-slot:sub>减法</template>
</showinfo>
</div>
</template>
<script>
import showinfo from "./components/showinfo.vue"
export default {
components: {
showinfo: showinfo
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button><slot name="add"></slot></button> <!-- 指定slot名称 -->
<button><slot name="sub"></slot></button>
</div>
</template>
- 注意:
如果我们的插槽没有指定名称,其实,它默认有一个隐藏的名为"default"的名称;
渲染作用域
这里有一个问题,就是如果我们在父组件内,指定插槽的内容时,给定了一个Mustache语法,那么这个属性是属于父组件还是子组件内呢,在Vue官方其实给定了一个渲染作用域的说明,其内容是,父组件里面的<template>元素内的所有内容都是在父级作用域中编译,子组件里面的<template>元素内的所有内容都是在子级作用域中编译的,在这种场景下,这个属性属于父组件,如下示例;
<!-- 父组件 -->
<template>
<div>
<showinfo>
{{ buttonName }} <!-- 使用Mustache语法 -->
</showinfo>
</div>
</template>
<script>
import showinfo from "./components/showinfo.vue"
export default {
data() {
return {
buttonName: "按钮"
}
},
components: {
showinfo: showinfo
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button><slot></slot></button> <!-- 定义插槽 -->
</div>
</template>
作用域插槽
所谓作用域插槽,就是在父组件中,使用子组件的数据,但是需要知道的是,它并非子传父,而是插槽的一种数据传递方式,只不过和子传父有点类似,但是它是实现方式,其实有点特殊;
那么,如果想要在插槽中,实现向父组件传递数据,我们可以使用作用域插槽,作用域插槽就是在,子组件中的<slot>元素上,新增属性的方式向父组件传递数据;
然后父组件使用一种特殊的指令,能够拿到对应子组件中指定<slot>元素上的所有属性,所以,其实使用这个指令拿到的是一个对象,指令其实是有点特殊的,它有三部分组成,第一部分是slot指令,第二部分用来指定的slot名称,因为一个组件中slot插槽可以有多个,然后使用一个"="等于符号来指定接收这个对象的标识符,如v-slot:defalut="obj",就表示接收该组件中,插槽名为defalut的所有属性;
<!-- 父组件 -->
<template>
<div>
<showinfo>
<template v-slot:default="child_data"> <!-- 这里并非必须使用template元素,也可以直接写在组件元素 -->
{{ child_data.text }} <!-- 接收的是一个对象 -->
</template>
<template v-slot:one="child_data"> <!-- 这里并非必须使用template元素,也可以直接写在组件元素 -->
{{ child_data.text }} <!-- 接收的是一个对象 -->
</template>
</showinfo>
</div>
</template>
<script>
import showinfo from "./components/showinfo.vue"
export default {
components: {
showinfo: showinfo
}
}
</script>
<!-- 子组件 -->
<template>
<div>
<button>
<slot text="默认按钮"></slot> <!-- 向父组件传递一个属性 -->
</button>
<button>
<slot name="one" text="新增按钮"></slot> <!-- 向父组件传递一个属性 -->
</button>
</div>
</template>
Provide/Inject组件间通信
在此之前,实现组件间通信,主要是父子组件之间进行通信,但是问题是,我们的Vue的组件化开发,将我们的整个页面渲染成了一颗组件树,那么如果我们希望在当前组件中,引用祖先组件的数据呢,如下图,如果我们希望在,Banner组件中引用App组件中的数据,这种场景,如果我们使用父传子也可以实现,但是这样将非常的鸡肋,需要一个一个的传递;
那么,在这种,子组件中,需要引用祖先组件中的数据时,比较合适的方案有三种,第一种方案是使用Provide结合Inject,第二种方案是通过事件总线,第三种则是通过状态管理库Vuex来实现,此处主要讲述第一种方案;
在Vue框架内提供了Provide和Inject两个机制,它的主要作用就是实现多层级的父子关系组件间通信的,它的实现机制是,首先在数据提供方(祖先组件)使用Provide属性来暴露数据,然后在引用方(子组件)使用Inject属性来获取数据,任何后代的组件,无论层级有多深,都可以使用Inject属性来获取数据,如下图;
那么在祖先组件中去暴露数据,主要是通过组件对象的provide属性来定义,它是一个可以是一个对象,也可以是一个函数,但是对象的方式弊端有点多,在此就不做过多的赘述,那么当provide属性是一个函数时,该函数必须返回一个对象,来表示,当前组件需要向子组件暴露的数据;
那么当祖先组件暴露完数据之后,我们可以通过子组件对象的inject属性,来获取这个数据,inject属性是一个数组,数组的元素,就是需要从祖先组件的provide中获取的属性名称,如下示例;
<!-- app组件 -->
<template>
<div>
<app_main></app_main>
</div>
</template>
<script>
import app_main from "./components/app_main.vue"
export default {
data() {
return {
content: "组件间通信"
}
},
components: {
app_main
},
provide() {
return {
content: this.content // 暴露data函数内的数据
}
}
}
</script>
<!-- app_main组件 -->
<template>
<div>
<main_banner></main_banner>
</div>
</template>
<script>
import main_banner from "./main_banner.vue"
export default {
components: {
main_banner
}
}
</script>
<!-- main_banner子组件 -->
<template>
<div>{{ content }}</div>
</template>
<script>
export default {
inject: ["content"] // 获取祖先组件中使用provide属性暴露的数据
}
</script>
别名及默认值
在子组件中主要是inject属性来获取祖先组件中使用provide暴露的数据,但是这里有一个问题,就是,如果祖先祖件中使用provide暴露的数据,和自己内部data属性或者props属性的名称重名时,就会有问题,同时,如果祖先组件中,并没有使用provide暴露任何数据时,子组件又会出现标识符找不到的问题;
那么解决这个问题,就需要在子组件中将inject属性编写为一个对象,这个对象内部的属性名就是一个别名,可以在本组件中使用,这个别名的值,也是一个对象,这个对象内部可以有两个属性,第一个属性为from,表示该别名与祖先组件使用provide暴露的哪个数据对应,第二个属性为defalut,表示默认值;
那么为了笔记的整洁性,此处就直接展示子组件的内容,父组件及祖先组件内容不变,如下示例;
<!-- main_banner子组件 -->
<template>
<div>{{ app_content }}</div>
</template>
<script>
export default {
inject: {
app_content: { // 别名
from: "content", // 对应祖先组件使用provide暴露的数据
default: "默认值" // 默认值
}
}
}
</script>
- 注意:
如果需要处理响应式数据,即,祖先组件数据发生变化,子孙组件同时修改,需要显式提供一个computed的计算属性,具体使用,请查看官方文档,不在此做过多的赘述;
生命周期
生命周期起初是生物上面的一个概念,指代的是一个生物体从生命开始到结束周而复始所经历的一系列变化的过程,那对Vue来讲,与其说是Vue的生命周期,不如说是其内组件的生命周期,简单来说,它的生命周期就是用来描述一个组件从引入到退出的全过程,那复杂来说呢,就是一个组件从创建开始经历了数据初始化、挂载、更新等步骤后,直到最后被销毁的整个过程;
生命周期函数
那么在这每个生命周期的过程阶段中,Vue又提供了很多钩子函数,这些钩子函数,会被Vue源码在某个生命周期阶段内进行回调,所以通过这些钩子函数,我们可以直到目前Vue的组件正在经历哪一个阶段,同时,我们也可以在某一个阶段内加入一段逻辑,从而实现一些特殊的功能,比如在页面正式渲染到页面之前,先去服务端获取一段数据,实现预处理等一系列功能的实现,如下图;
如上图,这就是整个Vue框架内组件的生命周期图,同时Vue官方也给我们提供了八个钩子函数,这八个钩子函数,都是组件对象中的一个属性,和data、methods平级,根据上图,我们基本已经可以了解到,每个钩子函数会在不同的阶段进行回调,具体如下;
beforeCreate:该函数执行在组件创建、数据观测 (data observer) 和 event/watcher 事件配置之前,实例初始化之后被调用,在该阶段组件未创建,不能访问数据,组件中的data,ref均为undefined;
created:该函数在组件创建完成后被立即调用,在这一步,实例已完成数据观测 (data observer),属性和方法的运算,watch/event 事件回调,但是还未渲染成HTML模板,组件中的data对象已经存在,可以对data进行操作了,即可以访问数据,发请求,ref依旧是undefined,挂载阶段还没开始,$el 属性目前尚不可用;
beforeMount:该函数在组件挂载之前,在该阶段页面上还没渲染出 HTML 元素,data 初始化完成,ref 依旧不可以操作,相关的 render 函数首次被调用,可以访问数据,编译模板结束,虚拟 dom 已经存在,该钩子在服务器端渲染期间不被调用;
mounted:该函数是页面完成挂载之后执行的,这时 el 被新创建的 vm.$el 替换了,就可以操作 ref 了,一般会用于将组件初始时请求数据的方法放到这里面,filter 也是在这里生效;
beforeUpdate:该函数在数据更新时调用,发生在虚拟DOM打补丁之前,在有特殊需求的情况下,可以将更新之前的数据存起来,放到后面去使用,这里适合在更新之前访问现有的DOM;
updated:由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子,在数据更新之后做一些处理,即监控数据的变化,当这个钩子被调用时,组件DOM已经更新,所以你现在可以执行依赖于DOM的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或watcher取而代之;
beforeUnmount:该函数在实例销毁之前调用,这里的ref依旧可以操作,实例仍然完全可用,可以在这里做清除定时器的操作,防止内存泄漏;
unmounted:该函数在组件销毁的时候执行,即实例销毁后调用,这里的 ref 不存在,该钩子被调用后,对应 Vue 实例的所有指令都被解绑,所有的事件监听器被移除,所有的子实例也都被销毁;
如下示例,通过beforeUpdate和updated两个钩子函数,来监控view层的数据变化;
<template>
<div>
<p ref="names">{{ name }}</p>
<button v-on:click="name='cfj'">改变数据</button>
</div>
</template>
<script>
export default {
data() {
return {
name: "cce"
}
},
beforeUpdate() {
console.log(this.$refs.names.innerHTML)
},
updated() {
console.log(this.$refs.names.innerHTML)
}
}
</script>
元素选择器
在Vue里面不推荐使用原生JavaScript语法去操作DOM,所以Vue官方提供了一个$refs的属性,如果我们希望在某一个组件里面去获取到某一个元素对象,或者某个子组件的实例这个时候,我们可以给元素绑定一个ref的属性,然后可以在methods或者其他类型的处理函数内使用this.$refs.ref_value的形式去找到这个元素,如下示例;
<template>
<div>
<p ref="pele">测试</p> <!-- 定义ref属性-->
</div>
</template>
<script>
export default {
mounted() {
console.log(this.$refs.pele) // 使用this.$refs.ref_value的形式拿到这个元素
}
}
</script>
- 注意:
这里需要注意的是,如果ref属性被绑定在组件上,那么我们可以使用"this.$refs.ref_value.$el"获取到组件内的根节点,仅能获取到根节点,但是,我们也可以通过"this.$refs.ref_value.component_method_name"的形式来获取到对应的方法函数;
动态组件
所谓动态组件,就是根据不同的条件,动态的展示不同的组件,这个需求在此之前,我们可以使用v-if指令来实现,但是其过程比较复杂,代码冗余度也较高,因此,Vue框架给我们提供了一种动态组件的功能,能够根据不同的需求来展示不同的组件;
动态组件需要借助一个<component>的元素,并且在这个元素上加入一个is属性,该属性的值为需要展示的组件名称,如下示例;
<template>
<div>
<button v-on:click="currentComponentName='component_1'">component_1</button>
<button v-on:click="currentComponentName='component_2'">component_2</button>
<button v-on:click="currentComponentName='component_3'">component_3</button>
<component :is="currentComponentName"></component>
</div>
</template>
<script>
import component_1 from './components/component_1.vue'
import component_2 from './components/component_2.vue'
import component_3 from './components/component_3.vue'
export default {
data() {
return {
currentComponentName: component_1 // 默认展示组件名为component_1的组件
}
},
components: {
component_1,
component_2,
component_3
}
}
</script>
参数传递
如果我们在使用动态组件的过程中,希望使用父子间数据通信,其实和之前没什么两样,直接在<component>元素上面绑定对应的属性即可,如下示例;
<!--父组件-->
<template>
<div>
<button v-on:click="currentComponentName='component_1'">component_1</button>
<component content="父传子" v-on:component_click="componentClick" :is="currentComponentName"></component>
</div>
</template>
<script>
import component_1 from './components/component_1.vue'
export default {
data() {
return {
currentComponentName: undefined
}
},
methods: {
componentClick(payload) {
console.log(payload)
}
},
components: {
component_1
}
}
</script>
<!--子组件-->
<template>
<div>
<p>component_1,{{ content }}</p>
<button v-on:click="btnClick">子传父</button>
</div>
</template>
<script>
export default {
props: ["content"],
methods: {
btnClick() {
this.$emit("component_click", '子传父')
}
}
}
</script>
组件保持存活
拿动态组件举例,如果一个页面中有两个或者多个组件会频繁的来回切换,对于这种场景,如果较为频繁,不管是服务端还是客户端,多多少少都会带来极大的性能损耗,因为每一次切换组件就会执行一次组件都生命周期,从撞见到销毁,并且每个页面在渲染的那一刻可能都需要先向服务端获取数据,给服务端带来的性能消耗也非常大;
那么对于这种场景,Vue框架给我们提供了一种组件保持存活的技术,在组件切换的那一刻不会真正的销毁组件,而是将它在后台缓存起来,当再次切换到该组件时,就不需要重新走一边生命周期的过程;
那么这种组件保存存活的技术,需要借助一个<keep-alive>的元素来实现,被<keep-alive>元素包裹的组件将始终保存存活,就相当于快照一样,因此它还有一个好处,就是类似yield一样,可以恢复页面到切换前的那一刻,如下示例;
<!--父组件-->
<template>
<div>
<button v-on:click="currentComponentName='component_1'">component_1</button>
<button v-on:click="currentComponentName='component_2'">component_2</button>
<keep-alive>
<component :is="currentComponentName"></component>
</keep-alive>
</div>
</template>
<script>
import component_1 from './components/component_1.vue'
import component_2 from './components/component_2.vue'
export default {
data() {
return {
currentComponentName: 'component_1'
}
},
components: {
component_1,
component_2
}
}
</script>
<!--子组件-->
<template>
<div>
<p>component_1,当前值为:{{ num }}</p>
<button v-on:click="num += 1">加</button>
</div>
</template>
<script>
export default {
data() {
return {
num: 0
}
}
}
</script>
<!--子组件-->
<template>
<div>
<p>component_2</p>
</div>
</template>
- 注意:
需要注意的是,它必须配合我们的动态组件来实现保持存活;
指定保活组件
因为组件保持存活必须配合动态组件来实现,所以,其实我们很难控制到底谁该缓存,谁不该缓存,因为动态组件是根据条件的变化而变化的,所以,在<keep-alive>元素上,可以指定两个属性来确保哪些组件保活,哪些组件不保活,使用include可以指定哪些组件保活,使用exclude属性可以指定哪些组件不保活,这两个属性的值,可以是字符串,也可以是数组;
如果为字符串时,多个组件名称使用逗号分割,如果是数组时,直接将组件名称作为数组的元素即可,但是这里需注意的是,这里的组件名称并非注册时的组件名称,而是组件对象内的name属性值,如下示例;
<keep-alive include="component_1,component_2"> <!--这里的值为组件对象内的name属性值,并非组件名-->
<component_1></component_1>
</keep-alive>
<keep-alive :include="[component_1,component_2]"> <!--这里的值为组件对象内的name属性值,并非组件名-->
<component_1></component_1>
</keep-alive>
生命周期
对于组件保持存活,Vue官方提供了两个生命周期来监控,即activated和deactivated,它们分别表示,进入活跃状态时,和非活跃状态时,它们和其他生命周期的钩子函数一样,都是组件对象中的一个属性,和data、methods平级;
这里唯一需要注意的就是,这两个生命周期是存在于子组件内的,并非在父组件,表示当前组件目前是否活跃;