Vue3 之 动态组件和KeepAlive组件

一、动态组件

1、简介

​ 在某些业务场景下,页面的某模块具有多个组件但在同一时间只显示一个,需要在多个组件之间进行频繁的切换,如:tab切换等场景。除了可以使用v-ifv-show根据不同条件显示不同组件之外,还可以通过动态组件<component>来实现相同的效果。<component>虽然被称为动态组件,但其并非是内置组件,而是属于模板语法,在模板编译阶段会被编译。

​ 动态组件允许在同一挂载节点动态切换多个组件,可以根据具体条件,动态决定显示的组件。比起v-if/v-show的实现方式来说,无需创建多个挂载节点,且代码量更少。

​ 动态组件默认只保持当前组件存活,其余被切换掉的组件会被卸载,但可以结合<KeepAlive>组件实现被切换掉的组件保持存活状态。

2、基础用法

​ 动态组件的核心在于<component>标签和is属性,Vue会根据is属性的值来决定具体渲染在<component>标签位置上的是哪个组件。

html 复制代码
<component :is="son1"></component>

​ 在通过is属性指定展示的子组件时,is属性的值可以是组件在引入到当前组件时定义的注册名称(String类型,常在选项式API中使用),也可以是组件本身的定义(Component类型,常在组合式API中使用)。

组合式API中使用组件本身的定义决定渲染组件:

​ 在组合式API中,如果我们需要使用变量存储导入的子组件实例,如果使用ref,则控制台会抛出warn。因为ref是将组件实例转换为响应式对象,可能会导致不必要的性能开销,建议使用markRaw(对象本身)或shallowRef(浅层响应式)来避免这种情况。

html 复制代码
<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中使用组件注册名决定渲染组件:
html 复制代码
<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>标签可以写成双标签的形式,内部包含其他内容。

html 复制代码
<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等)作为要渲染的内容,但实际上这种场景并不多见,因此就不展开叙述了。

html 复制代码
<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()宏方法,进行相应操作。

父组件:
html 复制代码
<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>
子组件:
html 复制代码
<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元素,且是inputtextareaselect等本身可以使用v-model的元素,则v-model的双向绑定功能不会起作用。如果需要实现双向绑定,则可以手动通过对应的attribute和事件来实现。

html 复制代码
<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组件内部包裹动态组件时,如果动态组件发生的切换,那被切走的组件默认会被缓存,当再次切换回该组件时,将从缓存中将组件取出,重新显示。

html 复制代码
<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指令结合使用,保证组件内部在同一时间节点只能有一个组件作为直接子节点。

html 复制代码
<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组件有三个可以指定的属性,分别为:includeexcludemax

ts 复制代码
interface KeepAliveProps {
  /**
   * 如果指定,则只有与 `include` 名称
   * 匹配的组件才会被缓存。
   */
  include?: MatchPattern
  /**
   * 任何名称与 `exclude`
   * 匹配的组件都不会被缓存。
   */
  exclude?: MatchPattern
  /**
   * 最多可以缓存多少组件实例。
   */
  max?: number | string
}

type MatchPattern = string | RegExp | (string | RegExp)[]
include:

KeepAlive组件默认会缓存内部所有非活跃组件实例,但缓存过多的组件实例也会占用过多的内存资源,因此可以通过include属性显式的指定要被缓存的组件,未被指定的组件则不会被缓存。

include属性的值可以是英文逗号分割的字符串,或者一个正则表达式,以及包含两种类型的数组。如果属性值为后两者,则需要使用v-bind进行绑定。

html 复制代码
<!-- 属性值为字符串 如果要缓存多个组件 需要以英文逗号分割的 注意分隔符前后不加空格 -->
<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

html 复制代码
<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

html 复制代码
<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语法来指定:

html 复制代码
<!-- 方法一: 通过新的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属性正好相反,用于指定哪些组件不会被缓存。属性值类型也相同,可以是英文逗号分割的字符串,或者一个正则表达式,以及包含两种类型的数组。

html 复制代码
<!-- 属性值为字符串 如果要指定不缓存多个组件 需要以英文逗号分割的 注意分隔符前后不加空格 -->
<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个组件实例,避免过渡占用内存资源。

html 复制代码
<!-- 属性值为2 只会缓存最近切换的两个组件实例 -->
<KeepAlive max="2">
	<component :is="currentComponent" />
</KeepAlive>
4、生命周期

​ 当<KeepAlive>内部组件初始挂载时,挂载的组件会进入活跃状态,如果发生组件切换,组件实例会从DOM上移除,但组件会被<KeepAlive>缓存,组件状态变为不活跃状态,当组件重新被激活,挂载到DOM中时,组件状态会再次变为活跃状态。

​ 针对被缓存组件的这两种状态变化,Vue提供了对应的生命周期钩子函数供开发者调用:

使用<script setup>的组件:
html 复制代码
<script setup>
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  // 调用时机为首次挂载
  // 以及每次从缓存中被激活时调用
})

onDeactivated(() => {
  // 在及组件卸载时
  // 以每次进入缓存时调用
})
</script>
未使用<script setup>的组件:
html 复制代码
<script>
export default {
  name: 'KeepaliveSon2',
  activated() {
    // 调用时机为首次挂载
    // 以及每次从缓存中被激活时调用
    console.log('子组件2被挂载/激活了');
  },
  deactivated() {
    // 在及组件卸载时
    // 以每次进入缓存时调用
    console.log('子组件2被缓存/卸载了');
  },
};
</script>

<KeepAlive>内部发生组件切换时,会先触发被缓存组件的Deactivated钩子函数,再触发要激活组件的Activated钩子函数。但如果项目使用了服务端渲染,则这两个钩子函数在服务器端渲染期间不会被触发。

​ 如果<KeepAlive>缓存的组件内部还嵌套有其他后代组件,则后代组件也可以使用这两个生命周期钩子函数。在被激活时,后代组件的Activated钩子函数先触发,再触发根组件的Activated钩子函数;在被卸载时也是一样,后代组件的Deactivated钩子函数先触发,再触发根组件的Deactivated钩子函数。

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅7 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax