一、动态组件
1、简介
在某些业务场景下,页面的某模块具有多个组件但在同一时间只显示一个,需要在多个组件之间进行频繁的切换,如:tab切换等场景。除了可以使用v-if
、v-show
根据不同条件显示不同组件之外,还可以通过动态组件<component>
来实现相同的效果。<component>
虽然被称为动态组件,但其并非是内置组件,而是属于模板语法,在模板编译阶段会被编译。
动态组件允许在同一挂载节点动态切换多个组件,可以根据具体条件,动态决定显示的组件。比起v-if
/v-show
的实现方式来说,无需创建多个挂载节点,且代码量更少。
动态组件默认只保持当前组件存活,其余被切换掉的组件会被卸载,但可以结合<KeepAlive>
组件实现被切换掉的组件保持存活状态。
2、基础用法
动态组件的核心在于<component>
标签和is
属性,Vue会根据is
属性的值来决定具体渲染在<component>
标签位置上的是哪个组件。
<component :is="son1"></component>
在通过is
属性指定展示的子组件时,is
属性的值可以是组件在引入到当前组件时定义的注册名称(String
类型,常在选项式API中使用),也可以是组件本身的定义(Component
类型,常在组合式API中使用)。
组合式API中使用组件本身的定义决定渲染组件:
在组合式API中,如果我们需要使用变量存储导入的子组件实例,如果使用ref
,则控制台会抛出warn
。因为ref
是将组件实例转换为响应式对象,可能会导致不必要的性能开销,建议使用markRaw
(对象本身)或shallowRef
(浅层响应式)来避免这种情况。
<template>
<div>
<h1>这就是动态组件</h1>
<!-- 根据按钮点击切换组件 -->
<button v-for="(item, index) in components" :key="index" @click="currentComponent = item">{{ index }}</button>
<!-- 使用动态组件 -->
<component :is="currentComponent" />
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
// 导入子组件
import son1 from '../components/dtzj-son1.vue'
import son2 from '../components/dtzj-son2.vue'
import son3 from '../components/dtzj-son3.vue'
// 定义当前展示的子组件 值为对子组件的引用
const currentComponent = shallowRef(son1)
// 定义子组件对象
const components = {
son1,
son2,
son3
}
// 2秒后切换显示的子组件
setTimeout(() => {
currentComponent.value = son3
}, 2000)
</script>
选项式API中使用组件注册名决定渲染组件:
<template>
<component :is="view" />
</template>
<script>
import Foo from './Foo.vue'
import Bar from './Bar.vue'
export default {
components: { Foo, Bar },
data() {
return {
view: 'Foo'
}
}
}
</script>
3、渲染普通HTML元素
is
属性的值还可以是普通的HTML标签名(不包含<>
),但是要以字符串的形式设置。Vue会根据字符串的值渲染对应的HTML标签。<component>
标签可以写成双标签的形式,内部包含其他内容。
<template>
<div>
<!-- 渲染普通HTML -->
<component v-for="(item, index) in htmlList" :key="index" :is="item">
{{ item+'标签' }}
</component>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
// 定义要渲染的html标签
const htmlList = ref(['a','span','div','h5','p','i','aside'])
</script>
页面效果:
4、渲染内置组件
动态组件还可以将内置组件(<Transition>
、Teleport
等)作为要渲染的内容,但实际上这种场景并不多见,因此就不展开叙述了。
<template>
<div>
<!-- 渲染内置组件 -->
<component :is="Transition">
<div v-show="show">这里是需要过渡的内容</div>
</component>
<!-- 等同于 -->
<Transition>
<div v-show="show">这里是需要过渡的内容</div>
</Transition>
</div>
</template>
<script setup lang="ts">
// 导入内置组件
import { ref, Transition } from 'vue'
// 定义div的显/隐
const show = ref(false)
// 3秒后切换div的显隐状态
setTimeout(() => {
show.value = true
}, 3000)
</script>
<style scoped>
/* 定义过渡样式 */
.v-enter-active,
.v-leave-active {
transition: opacity 0.5s ease;
}
.v-enter-from,
.v-leave-to {
opacity: 0;
}
</style>
5、v-model的特殊性
在<component>
标签上使用 v-model
时,模板编辑器会将其扩展为modelValue
的prop和update:modelValue
的事件监听器,而并非原始的v-model
双向绑定功能。
如果渲染的普通自定义子组件,内部可以接收prop和使用事件监听器,或者使用defineModel()
宏方法,进行相应操作。
父组件:
<template>
<div>
<!-- 使用的动态组件 并使用v-mode -->
<component :is="currentComponent" v-model="test" />
<h3>{{ test }}</h3>
</div>
</template>
<script setup lang="ts">
import { shallowRef } from 'vue'
// 导入子组件
import son1 from '../components/dtzj-son1.vue'
// 定义当前展示的子组件 值为对子组件的引用
const currentComponent = shallowRef(son1)
// 定义一个变量
const test = ref('test')
</script>
子组件:
<template>
<div>
<h5>{{ modelVar }}</h5>
</div>
</template>
<script setup lang="ts">
import { onMounted } from 'vue'
const modelVar = defineModel();
onMounted(() => {
setTimeout(() => {
modelVar.value = '子组件更改了modelVar';
}, 1000)
})
</script>
但如果渲染的是普通HTML元素,且是input
、textarea
、select
等本身可以使用v-model
的元素,则v-model
的双向绑定功能不会起作用。如果需要实现双向绑定,则可以手动通过对应的attribute
和事件来实现。
<script setup>
import { ref } from 'vue'
const tag = ref('input')
const username = ref('')
</script>
<template>
<!-- 由于 'input' 是原生 HTML 元素,因此这个 v-model 不起作用 -->
<component :is="tag" v-model="username" />
</template>
二、KeepAlive
1、简介
在上面的内容中,讲解了动态组件的相关知识,但是在默认情况下,通过动态组件被切走的组件,会被卸载后被销毁,其内部的所有变动过的状态会丢失,等再次切换回该组件时,则会重新创建该组件的组件实例。但在某些场景下,我们不希望被切走的组件被销毁,并且保留其内部状态,那此时就需要借助KeepAlive
内置组件。
将KeepAlive
组件包裹在动态组件的外层,当动态组件发生切换时,默认会将所有被切走的非活跃组件进行缓存,而不是销毁,并且组件内部的状态也会被保留。
在需要频繁反复切换动态组件的业务场景中,如:tab切换、路由转换等,使用KeepAlive
组件可以减少组件实例的销毁和创建过程,从而优化页面的性能。
2、基础用法
KeepAlive
组件内部可以包裹动态组件,也可以包裹普通组件。但KeepAlive
组件在任何时间节点,只能有一个活跃组件作为其直接子节点,不允许多个组件共存作为直接子节点。
动态组件:
当KeepAlive
组件内部包裹动态组件时,如果动态组件发生的切换,那被切走的组件默认会被缓存,当再次切换回该组件时,将从缓存中将组件取出,重新显示。
<template>
<div>
<KeepAlive>
<component :is="currentComponent" />
</KeepAlive>
</div>
</template>
<script setup lang="ts">
import { shallowRef, KeepAlive } from 'vue'
import son1 from '../components/keepalive-son1.vue'
import son2 from '../components/keepalive-son2.vue'
// 定义当前展示的子组件 值为对子组件的引用
const currentComponent = shallowRef(son1)
// 3秒切换显示的子组件
setTimeout(() => {
currentComponent.value = son2
}, 3000)
</script>
普通组件:
当KeepAlive
组件内部包裹普通组件时,通常与v-if
/v-else-if
/v-else
指令结合使用,保证组件内部在同一时间节点只能有一个组件作为直接子节点。
<template>
<div>
<KeepAlive>
<son1 v-if="show === 1"></son1>
<son2 v-else></son2>
</KeepAlive>
</div>
</template>
<script setup lang="ts">
import { ref, KeepAlive } from 'vue'
import son1 from '../components/keepalive-son1.vue'
import son2 from '../components/keepalive-son2.vue'
// 决定显示的组件
const show = ref(1)
// 3秒后切换显示的子组件
setTimeout(() => {
// currentComponent.value = son2
show.value = 2;
}, 3000)
</script>
注意: 不能使用v-show
指令,因为其仅仅是通过设置display
属性实现的元素显隐,其节点依旧保留在DOM中,实际上会让KeepAlive
组件内部同时存在多个直接子节点,从而引发报错。
3、组件属性
KeepAlive
组件有三个可以指定的属性,分别为:include
、exclude
和max
。
interface KeepAliveProps {
/**
* 如果指定,则只有与 `include` 名称
* 匹配的组件才会被缓存。
*/
include?: MatchPattern
/**
* 任何名称与 `exclude`
* 匹配的组件都不会被缓存。
*/
exclude?: MatchPattern
/**
* 最多可以缓存多少组件实例。
*/
max?: number | string
}
type MatchPattern = string | RegExp | (string | RegExp)[]
include:
KeepAlive
组件默认会缓存内部所有非活跃组件实例,但缓存过多的组件实例也会占用过多的内存资源,因此可以通过include
属性显式的指定要被缓存的组件,未被指定的组件则不会被缓存。
include
属性的值可以是英文逗号分割的字符串,或者一个正则表达式,以及包含两种类型的数组。如果属性值为后两者,则需要使用v-bind
进行绑定。
<!-- 属性值为字符串 如果要缓存多个组件 需要以英文逗号分割的 注意分隔符前后不加空格 -->
<KeepAlive include="keepalive-son1,keepalive-son2">
<component :is="currentComponent" />
</KeepAlive>
<!--属性值为数组形式 指定多个组件 -->
<KeepAlive :include="['keepalive-son1', 'keepalive-son2']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 属性值为正则表达式 符合匹配条件的组件会被缓存 -->
<KeepAlive :include="/^keepalive-son/">
<component :is="currentComponent" />
</KeepAlive>
<script setup lang="ts">
import { ref, shallowRef, KeepAlive } from 'vue'
// 注意这里的 son1 是在当前组件内的注册名称 组件本身生成的name属性为 keepalive-son1
import son1 from '../components/keepalive-son1.vue'
import son2 from '../components/keepalive-son2.vue'
// 定义当前展示的子组件 值为对子组件的引用
const currentComponent = shallowRef(son1)
// 3秒后切换显示的子组件
setInterval(() => {
currentComponent.value === son1
? (currentComponent.value = son2)
: currentComponent.value = son1
}, 3000)
</script>
该属性的属性值会与子组件的name
选项进行匹配,在选项式API中必须显示的声明name
选项,在组合式API中使用<script setup>
的组件会根据文件名称隐式的生成完全相同的name
选项,无需手动声明。
使用<script setup>
的组件keepalive-son1.vue
:
<template>
<div>
<p>这是子组件1中的count:{{ count }}</p>
<button @click="count++">add</button>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const count = ref(1111)
// 此时组件没有显式指定name属性 则会隐式的设置name的值为 keepalive-son1
// KeepAlive 会根据 keepalive-son1 进行识别
</script>
非使用<script setup>
的组件keepalive-son2.vue
:
<template>
<div>
<p>这是子组件2中的count:{{ count }}</p>
<button @click="count++">add</button>
</div>
</template>
<script>
export default {
// 显式的指定name属性
name: 'KeepaliveSon2',
data() {
return {
count: 2222
};
},
};
</script>
如果在使用<script setup>
的单文件组件中,想要显式的指定组件的name
属性,可以通过defineOptions()
宏方法或者export default
语法来指定:
<!-- 方法一: 通过新的script + export default 定义组件的name属性 -->
<!-- 注意两个 script 的 lang 属性要一致 -->
<script lang="ts">
export default {
name: 'KeepaliveSon1',
}
</script>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
const count = ref(1111)
// 方法二: 通过 defineOptions 定义组件的name属性
defineOptions({
name: 'KeepaliveSon1',
})
</script>
exclude:
exclude
属性与include
属性正好相反,用于指定哪些组件不会被缓存。属性值类型也相同,可以是英文逗号分割的字符串,或者一个正则表达式,以及包含两种类型的数组。
<!-- 属性值为字符串 如果要指定不缓存多个组件 需要以英文逗号分割的 注意分隔符前后不加空格 -->
<KeepAlive exclude="KeepaliveSon1,KeepaliveSon2">
<component :is="currentComponent" />
</KeepAlive>
<!--属性值为数组形式 指定多个组件不缓存 -->
<KeepAlive :exclude="['keepalive-son1', 'keepalive-son2']">
<component :is="currentComponent" />
</KeepAlive>
<!-- 属性值为正则表达式 符合匹配条件的组件不会被缓存 -->
<KeepAlive :exclude="/^keepalive-son/">
<component :is="currentComponent" />
</KeepAlive>
属性值会与子组件的name
选项进行匹配,在选项式API中必须显示的声明name
选项,在组合式API中使用<script setup>
的组件会根据文件名称隐式的生成完全相同的name
选项,无需手动声明。如果想要显式的指定name
属性,可以通过defineOptions()
宏方法或者export default
语法来指定。
max:
该属性用于指定KeepAlive
能缓存的组件实例数量,属性值为一个非负整数。当切换的组件数量大于max
属性值时,会自动将最先缓存的组件实例销毁,只保留最近缓存的max
个组件实例,避免过渡占用内存资源。
<!-- 属性值为2 只会缓存最近切换的两个组件实例 -->
<KeepAlive max="2">
<component :is="currentComponent" />
</KeepAlive>
4、生命周期
当<KeepAlive>
内部组件初始挂载时,挂载的组件会进入活跃状态,如果发生组件切换,组件实例会从DOM上移除,但组件会被<KeepAlive>
缓存,组件状态变为不活跃状态,当组件重新被激活,挂载到DOM中时,组件状态会再次变为活跃状态。
针对被缓存组件的这两种状态变化,Vue提供了对应的生命周期钩子函数供开发者调用:
使用<script setup>
的组件:
<script setup>
import { onActivated, onDeactivated } from 'vue'
onActivated(() => {
// 调用时机为首次挂载
// 以及每次从缓存中被激活时调用
})
onDeactivated(() => {
// 在及组件卸载时
// 以每次进入缓存时调用
})
</script>
未使用<script setup>
的组件:
<script>
export default {
name: 'KeepaliveSon2',
activated() {
// 调用时机为首次挂载
// 以及每次从缓存中被激活时调用
console.log('子组件2被挂载/激活了');
},
deactivated() {
// 在及组件卸载时
// 以每次进入缓存时调用
console.log('子组件2被缓存/卸载了');
},
};
</script>
<KeepAlive>
内部发生组件切换时,会先触发被缓存组件的Deactivated
钩子函数,再触发要激活组件的Activated
钩子函数。但如果项目使用了服务端渲染,则这两个钩子函数在服务器端渲染期间不会被触发。
如果<KeepAlive>
缓存的组件内部还嵌套有其他后代组件,则后代组件也可以使用这两个生命周期钩子函数。在被激活时,后代组件的Activated
钩子函数先触发,再触发根组件的Activated
钩子函数;在被卸载时也是一样,后代组件的Deactivated
钩子函数先触发,再触发根组件的Deactivated
钩子函数。