Suspense
Suspense 是 Vue 3 的一个内置组件,用于在组件树中协调异步依赖的处理。它不是处理单个组件的加载状态,而是能在组件树的上层,等待下层多个嵌套异步依赖项解析完成,并统一显示加载状态。
Suspense 的核心工作流程
Suspense 的三个核心状态:
- 初始渲染:在内存中渲染
#default插槽,同时收集异步依赖 - Pending 状态(挂起) :存在未完成的异步依赖,显示
#fallback内容 - Resolved 状态(完成) :所有依赖已完成,显示
#default的真实内容
如果初始渲染中没有遇到任何异步依赖,Suspense 会直接进入 Resolved 状态。
mountSuspense 的执行过程:
- 创建一个离屏(隐藏)DOM 容器,用于提前渲染
#default内容(异步组件等)。 - 创建
SuspenseBoundary实例,管理异步依赖、状态切换和 DOM 操作。 - 将
#default内容挂载到隐藏容器中(此时用户不可见)。 - 检查是否有未解决的异步依赖:
- 有依赖:挂载
#fallback到可见容器,用户看到加载状态;触发pending/fallback事件。 - 无依赖:立即 resolve,将隐藏容器中的
#default内容(vnode.el真实DOM)插入到可见容器,完成渲染。
- 有依赖:挂载
Suspense 可以等待的异步依赖类型
Suspense 能够识别并等待以下两类异步依赖:
- 带有
async setup()的组件:包括使用<script setup>并包含顶层await表达式的组件 - 异步组件:通过
defineAsyncComponent定义的组件
组件 Props
interface SuspenseProps {
// 触发时机:当所有异步依赖解析完成,Suspense 切换到 active 分支时
onResolve?: () => void
onPending?: () => void // 检测到异步依赖时触发
onFallback?: () => void // fallback 内容开始显示时触发
timeout?: string | number // 超时时间(毫秒),-1 表示永不超时
/**
* Allow suspense to be captured by parent suspense
* 是否允许被父 Suspense 捕获为依赖
* @default false
*/
suspensible?: boolean
}
示例 基本使用
<template>
<div>
<Suspense>
<AsyncComponent />
</Suspense>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() => import("./ComPropsA.vue"));
export default {
components: {
AsyncComponent,
},
};
</script>
import {defineAsyncComponent} from "/node_modules/.vite/deps/vue.js?v=32983e00";
const AsyncComponent = defineAsyncComponent( () => import("/src/pages/cloud/bugs/components/ComPropsA.vue"));
const _sfc_main = {
components: {
AsyncComponent
}
};
import {resolveComponent as _resolveComponent, createVNode as _createVNode, Suspense as _Suspense, withCtx as _withCtx, openBlock as _openBlock, createBlock as _createBlock, createElementBlock as _createElementBlock} from "/node_modules/.vite/deps/vue.js?v=32983e00";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_AsyncComponent = _resolveComponent("AsyncComponent");
return _openBlock(),
_createElementBlock("div", null, [(_openBlock(),
_createBlock(_Suspense, null, {
default: _withCtx( () => [_createVNode(_component_AsyncComponent)]),
_: 1
}))]);
}
示例 回调函数,超时设置
<template>
<div>
<Suspense
timeout="10"
@fallback="handleFallback"
@pending="handlePending"
@resolve="handleResolve"
>
<AsyncComponent />
</Suspense>
</div>
</template>
<script lang="ts">
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() => import("./ComPropsA.vue"));
export default {
components: {
AsyncComponent,
},
methods: {
handleFallback() {
console.log("handleFallback");
},
handlePending() {
console.log("handlePending");
},
// 组件挂载完成后,调用 handleResolve 方法
handleResolve() {
console.log("handleResolve");
},
},
mounted() {
console.log("mounted");
},
};
</script>
插槽
在 Vue 3 中,<Suspense> 组件通过两个具名插槽来管理加载状态:
#default:放置需要等待异步依赖(异步组件、async setup())的真实内容。#fallback:放置异步依赖加载期间显示的“加载中”内容(也就是 loading 状态)。- 错误处理通过
onErrorCaptured生命周期钩子实现,Suspense本身不捕获错误如果#default包含多个顶级元素,Vue 会在开发环境下发出警告。
如果 #default 插槽中只有同步组件(没有异步依赖),#fallback 永远不会显示。
Suspense 的核心机制是等待异步依赖解析。当它渲染 #default 插槽时:
- 同步组件会立即完成渲染,不会产生任何待处理的 Promise。
- 如果没有检测到任何异步依赖(如
defineAsyncComponent或async setup),Suspense 会直接进入 Resolved(完成) 状态。 - 此时
#fallback插槽根本不会被考虑,#default内容会直接显示。
示例 fallback
- 当
<Suspense>初次渲染或其内部异步依赖发生变化时,会自动显示#fallback插槽内容。 - 一旦所有异步依赖(如
AsyncComponent组件内部的defineAsyncComponent或顶层await)都已完成,便会自动切换为#default内容。
<template>
<div>
<Suspense
timeout="10"
@fallback="handleFallback"
@pending="handlePending"
@resolve="handleResolve"
>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Fallback />
</template>
</Suspense>
</div>
</template>
<script lang="ts">
import Fallback from "@/components/Fallback.vue";
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() => import("./ComPropsA.vue"));
export default {
components: {
AsyncComponent,
Fallback,
},
methods: {
handleFallback() {
console.log("handleFallback");
},
handlePending() {
console.log("handlePending");
},
// 组件挂载完成后,调用 handleResolve 方法
handleResolve() {
console.log("handleResolve");
},
},
mounted() {
console.log("mounted");
},
onErrorCaptured(error: ErrorEvent) {
console.log(error);
},
};
</script>
示例 suspense 结合 defineAsyncComponent
<template>
<div key="div--0">
<Suspense
timeout="10"
key="SuspenseD--0"
@fallback="handleFallback"
@pending="handlePending"
@resolve="handleResolve"
>
<template #default>
<AsyncComponent key="AsyncComponent-A" />
</template>
<template #fallback>
<Fallback key="Fallback-A" />
</template>
</Suspense>
</div>
</template>
<script lang="ts">
import Fallback from "@/components/Fallback.vue";
import { defineAsyncComponent } from "vue";
import Error from "@/components/Error.vue";
import Loading from "@/components/Loading.vue";
const AsyncComponent = defineAsyncComponent({
loader: () => import("./ComPropsA.vue"),
delay: 0,
loadingComponent: Loading,
errorComponent: Error,
suspensible: true, // false 加载显示loading; true 加载显示fallback
});
export default {
components: {
AsyncComponent,
Fallback,
},
methods: {
handleFallback() {
console.log("handleFallback");
},
handlePending() {
console.log("handlePending");
},
// 组件挂载完成后,调用 handleResolve 方法
handleResolve() {
console.log("handleResolve");
},
},
mounted() {
console.log("mounted");
},
onErrorCaptured(error: ErrorEvent) {
console.log(error);
},
};
</script>
编译结果
Suspence组件
Suspence组件虚拟节点的suspense属性
suspense.pendingBranch(ssContent)
解析组件 AsyncComponent-A 即之前的uspense.pendingBranch(ssContent
异步组件的 parentSuspense
异步组件 mountComponent
异步组件 创建组件实例
异步组件。执行 setupComponent(初始化props、slots),执行 setupStatefulComponent
异步组件 AsyncComponentWrapper 组件 执行组件 setup 函数
执行 suspense.registerDep 注册收集依赖
instance是异步组件、suspense 是 Suspense组件
suspense组件 dep大于 0
触发事件 onPending、onFallback,挂载 Suspense组件的 vnode.ssFallback,设置 vnode.ssFallback 为 suspense 的活跃分支activeBranch
mountSuspense 处理完毕,
当异步组件加载完毕
instance是异步组件,instance.asyncResolved表明异步组件已解析,执行handleSetupResult
instance.render
finishComponentSetup
setupRenderEffect 给组件更新创建副作用函数,执行一次 update,实现组件挂载
组件内部实例vode
defineAsyncComponent
defineAsyncComponent 是一个工厂函数,它接收一个加载器(loader)或一个配置对象,并返回一个组件对象。这个组件对象在渲染时会自动执行加载逻辑,并在加载过程中显示 loading / error 占位内容。
接受参数( 配置选项)
interface AsyncComponentOptions<T = any> {
/** 异步组件的加载函数 */
loader: AsyncComponentLoader<T>
/** 加载中显示的组件 */
loadingComponent?: Component
/** 加载失败显示的组件 */
errorComponent?: Component
/** 显示 loading 的延迟时间(默认 200ms) */
delay?: number
/** 超时时间(undefined 表示永不超时) */
timeout?: number
/** 是否可挂起(默认 true) */
suspensible?: boolean
/** SSR 水合策略 */
hydrate?: HydrationStrategy
/** 错误处理函数 */
onError?: (
error: Error,
retry: () => void,
fail: () => void,
attempts: number,
) => any
}
示例 一个加载函数
直接传入一个返回 Promise 的加载函数(通常配合动态导入 import())。
<template>
<div>
<AsyncComponent />
</div>
</template>
<script lang="ts">
// 1. 基本语法
import { defineAsyncComponent } from "vue";
const AsyncComponent = defineAsyncComponent(() => import("./ComPropsA.vue"));
// 2. 在组件中局部注册
export default {
components: {
AsyncComponent,
},
};
</script>
// 1. 基本语法
import {defineAsyncComponent} from "/node_modules/.vite/deps/vue.js?v=ebcf97fa";
const AsyncComponent = defineAsyncComponent( () => import("/src/pages/cloud/bugs/components/ComPropsA.vue?t=1778045779403"));
// 2. 在组件中局部注册
const _sfc_main = {
components: {
AsyncComponent
}
};
import {resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock} from "/node_modules/.vite/deps/vue.js?v=ebcf97fa";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_AsyncComponent = _resolveComponent("AsyncComponent");
return _openBlock(),
_createElementBlock("div", null, [_createVNode(_component_AsyncComponent)]);
}
示例 一个加载函数
<template>
<div key="SuspenseA--0">
<AsyncComA key="AsyncComA--0" />
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from "vue";
const AsyncComA = defineAsyncComponent(() => import("./ComPropsA.vue"));
</script>
示例 详细配置对象
<template>
<div>
<AsyncComponent />
</div>
</template>
<script lang="ts">
import { defineAsyncComponent } from "vue";
import Loading from "@/components/Loading.vue";
import Fallback from "@/components/Fallback.vue";
const AsyncComponent = defineAsyncComponent({
loader: () => import("./ComPropsA.vue"),
delay: 100,
loadingComponent: Loading,
errorComponent: Fallback,
});
export default {
components: {
AsyncComponent,
},
};
</script>
import {defineAsyncComponent} from "/node_modules/.vite/deps/vue.js?v=ebcf97fa";
import Loading from "/src/components/Loading.vue";
import Fallback from "/src/components/Fallback.vue";
const AsyncComponent = defineAsyncComponent({
loader: () => import("/src/pages/cloud/bugs/components/ComPropsA.vue?t=1778045779403"),
delay: 100,
loadingComponent: Loading,
errorComponent: Fallback
});
const _sfc_main = {
components: {
AsyncComponent
}
};
import {resolveComponent as _resolveComponent, createVNode as _createVNode, openBlock as _openBlock, createElementBlock as _createElementBlock} from "/node_modules/.vite/deps/vue.js?v=ebcf97fa";
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_AsyncComponent = _resolveComponent("AsyncComponent");
return _openBlock(),
_createElementBlock("div", null, [_createVNode(_component_AsyncComponent)]);
}
异步组件加载
示例 模拟 onError
<template>
<div key="div--0">
<AsyncComA key="AsyncComA--0" />
</div>
</template>
<script lang="ts" setup>
import { defineAsyncComponent } from "vue";
import LoaderError from "@/components/Error.vue";
const AsyncComA = defineAsyncComponent({
loader: () =>
new Promise((resolve, reject) => {
setTimeout(() => {
// 直接 reject,模拟网络错误
reject(new Error("AsyncComA 加载失败:模拟网络超时"));
}, 1000);
}),
errorComponent: LoaderError,
onError: (error, userRetry, userFail, retries) => {
console.info("AsyncComA 加载失败:", error, userRetry, userFail, retries);
// 重试3次
if (retries < 3) {
userRetry();
} else {
userFail();
}
},
});
</script>
suspensible
<Suspense> 的 fallback:一个树级别的加载占位符。当 <Suspense> 的默认插槽中存在任何异步依赖(如异步组件、setup 返回 Promise 的组件)时,fallback 会显示,直至所有异步依赖完成。
异步组件的 loadingComponent:一个组件级别的加载指示器。通过 defineAsyncComponent 定义异步组件时,可以指定 loadingComponent,当该组件自身正在加载时,会显示这个加载组件(前提是加载过程没有被外部的 <Suspense> “接管”)。
<template>
<div>
<Suspense
timeout="10"
@fallback="handleFallback"
@pending="handlePending"
@resolve="handleResolve"
>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<Fallback />
</template>
</Suspense>
</div>
</template>
<script lang="ts">
import Fallback from "@/components/Fallback.vue";
import { defineAsyncComponent } from "vue";
import Error from "@/components/Error.vue";
import Loading from "@/components/Loading.vue";
const AsyncComponent = defineAsyncComponent({
loader: () => import("./ComPropsA.vue"),
delay: 0,
loadingComponent: Loading,
errorComponent: Error,
suspensible: false, // false 加载显示loading; true 加载显示fallback
});
export default {
components: {
AsyncComponent,
Fallback,
},
methods: {
handleFallback() {
console.log("handleFallback");
},
handlePending() {
console.log("handlePending");
},
// 组件挂载完成后,调用 handleResolve 方法
handleResolve() {
console.log("handleResolve");
},
},
mounted() {
console.log("mounted");
},
onErrorCaptured(error: ErrorEvent) {
console.log(error);
},
};
</script>
情况一:suspensible: true(默认)
- 异步组件不会显示自己的
loadingComponent。 - 它会向外层抛出异步依赖,由最近的
<Suspense>统一管理加载状态。 <Suspense>的fallback插槽会被显示,直到组件加载完成。- 结果:只有
fallback生效,loadingComponent被忽略。
隐藏容器中挂载 默认内容
卸载旧 vnode(activeBranch 即 fallback node)
移动
- 移动前:
pendingBranch为 异步组件、activeBranch为fallback组件 - 移动完成: 挂载异步组件在实际DOM中。
activeBranch为异步组件内容 。pendingBranch为null
情况二:suspensible: false
- 异步组件会独立显示自己的
loadingComponent(或默认无加载提示)。 - 它不会通知外层的
<Suspense>等待自身,因此<Suspense>的fallback不会被触发(因为对 Suspense 来说,该子组件已经不是一个“异步依赖”)。 - 若
Suspense内部还有其他异步依赖,fallback仍会因那些依赖而显示;如果没有其他依赖,fallback则不会出现。 - 结果:
loadingComponent独立工作,外部fallback不受影响(除非没有其他依赖)。
Suspense 组件 vode
vnode.suspence = createSuspenseBoundary()
将主容器(suspense的默认插槽)挂载到隐藏容器
解析 suspense.resolve
在所有异步依赖都已解析完成后,立即同步地将待解析分支(pendingBranch)切换为激活分支(activeBranch)
在 Suspense 解析完成后,将等待中的内容(pendingBranch)从隐藏容器移动到实际的 DOM 容器中。
组件的真正 vnode 在 vnode.component.subTree
将 loading 挂载到实际容器上
defineComponent
defineComponent 返回一个组件对象,该对象可以直接用于 Vue 的 components 选项、动态组件或路由注册。
当 defineComponent 中同时存在 template 和 render 时,render 的优先级更高,template 会被忽略(仅开发环境可能发出警告)
示例 template
template 是字符串模板,需要编译器(运行时或构建时)转换为 render 函数
<script lang="ts">
import { defineComponent, ref } from "vue";
export default defineComponent({
name: "CustomA",
props: {
initial: {
type: Number,
default: 0,
},
},
emits: ["update"],
setup(props, { emit }) {
const count = ref(props.initial);
const increment = () => {
count.value++;
emit("update", count.value);
};
return { count, increment };
},
template: `
<button @click="increment">
{{ count }}
</button>
`,
});
</script>
runtime-core.esm-bundler.js:51 [Vue warn]:
Component provided template option but runtime compilation is not supported in this build of Vue.
Configure your bundler to alias "vue" to "vue/dist/vue.esm-bundler.js"
示例
<script lang="ts">
import { defineComponent, ref, h } from "vue";
export default defineComponent({
name: "CustomA",
props: {
initial: {
type: Number,
default: 1,
},
},
emits: ["update"],
setup(props, { emit }) {
const count = ref(props.initial);
const increment = () => {
count.value++;
emit("update", count.value);
};
return { count, increment };
},
render() {
return h("div", { class: "custom-a" }, [
h("button", { onClick: this.increment }, "button"),
h("p", {}, this.count),
]);
},
});
</script>
示例 setup 返回渲染函数
setup 返回的渲染函数:优先级最高,会覆盖 render 选项和 template。
<script lang="ts">
import { defineComponent, ref, h } from "vue";
export default defineComponent({
name: "CustomA",
props: {
initial: {
type: Number,
default: 1,
},
},
emits: ["update"],
setup(props, { emit }) {
const count = ref(props.initial);
const increment = () => {
count.value++;
emit("update", count.value);
};
// return { count, increment };
return () =>
h("div", { class: "custom-a" }, [
h("button", { onClick: increment }, "button"),
h("p", {}, count.value),
]);
},
});
</script>
Teleport
<Teleport> 是 Vue 3 提供的一个内置组件,它允许你将组件模板中的一部分内容“传送”到当前组件 DOM 树之外的任意位置(通常是 body 或任何指定的 DOM 元素),从而解决常见于模态框、通知提示、下拉菜单等需要脱离父容器样式或 z-index 限制的场景。
interface TeleportProps {
// 目标容器选择器或元素实例
to: string | RendererElement | null | undefined
// 是否禁用 Teleport 组件,为 true 时内容将保留在原位置
disabled?: boolean
// 是否延迟渲染 Teleport 组件
defer?: boolean
}
示例
<template>
<div>
<h2>这里是TeleportA</h2>
<Teleport to="body" key="TeleportA--0">
<div class="modal-overlay">
<div class="modal-content">这里是TeleportA 内容</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
编译结果
import {createElementVNode as _createElementVNode, Teleport as _Teleport, openBlock as _openBlock, createBlock as _createBlock, createElementBlock as _createElementBlock} from "/node_modules/.vite/deps/vue.js?v=b145d65a"
function _sfc_render(_ctx, _cache) {
return (_openBlock(),
_createElementBlock("div", null, [_cache[1] || (_cache[1] = _createElementVNode("h2", null, "这里是TeleportA", -1 /* CACHED */
)), (_openBlock(),
_createBlock(_Teleport, {
to: "body",
key: "TeleportA--0"
}, [_cache[0] || (_cache[0] = _createElementVNode("div", {
class: "modal-overlay"
}, [_createElementVNode("div", {
class: "modal-content"
}, "这里是TeleportA 内容")], -1 /* CACHED */
))]))]))
}
示例 disable
<template>
<div>
<h2>这里是TeleportA</h2>
<Teleport to="body" key="TeleportA--0" disabled>
<div class="modal-overlay">
<div class="modal-content">这里是TeleportA 内容</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts"></script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
编译结果
import {createElementVNode as _createElementVNode, Teleport as _Teleport, openBlock as _openBlock, createBlock as _createBlock, createElementBlock as _createElementBlock} from "/node_modules/.vite/deps/vue.js?v=b145d65a"
function _sfc_render(_ctx, _cache) {
return (_openBlock(),
_createElementBlock("div", null, [_cache[1] || (_cache[1] = _createElementVNode("h2", null, "这里是TeleportA", -1 /* CACHED */
)), (_openBlock(),
_createBlock(_Teleport, {
to: "body",
key: "TeleportA--0",
disabled: ""
}, [_cache[0] || (_cache[0] = _createElementVNode("div", {
class: "modal-overlay"
}, [_createElementVNode("div", {
class: "modal-content"
}, "这里是TeleportA 内容")], -1 /* CACHED */
))]))]))
}
示例
注意:to 的值必须在 <Teleport> 挂载时已经存在于 DOM 中,否则会抛出警告(可通过创建一个动态目标容器解决)。
<template>
<div>
<h2>这里是TeleportB</h2>
<Teleport to="#dynamic-target" key="TeleportB--0">
<div class="modal-overlay">
<div class="modal-content">这里是TeleportB 内容</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { onMounted } from "vue";
onMounted(() => {
let container = document.getElementById("dynamic-target");
if (!container) {
container = document.createElement("div");
container.id = "dynamic-target";
document.body.appendChild(container);
}
});
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
出现错误
[Vue warn]: Failed to locate Teleport target with selector "#dynamic-target".
Note the target element must exist before the component is mounted - i.e.
the target cannot be rendered by the component itself,
and ideally should be outside of the entire Vue component tree.
[Vue warn]: Invalid Teleport target on mount: null (object)
修复
<template>
<div>
<h2>这里是TeleportB</h2>
<Teleport v-if="targetExists" to="#dynamic-target" key="TeleportB--0">
<div class="modal-overlay">
<div class="modal-content">这里是TeleportB 内容</div>
</div>
</Teleport>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref } from "vue";
const targetExists = ref(false);
onMounted(() => {
let container = document.getElementById("dynamic-target");
if (!container) {
container = document.createElement("div");
container.id = "dynamic-target";
document.body.appendChild(container);
}
targetExists.value = true;
});
</script>
<style scoped>
.modal-overlay {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
KeepAlive
KeepAlive 是 Vue 内置的抽象组件,核心作用是缓存组件实例、复用已渲染的 DOM 节点,避免组件重复创建 / 销毁,提升组件切换性能(比如 Tab 切换、路由切换)。
- 被 KeepAlive 包裹的组件,切换时不会触发
unmounted,而是触发deactivated(失活); - 再次激活时不会触发
mounted,而是触发activated(激活),复用原有实例和 DOM。
KeepAlive 组件 Props
props: {
// 包含的组件
include: [String, RegExp, Array],
// 排除的组件
exclude: [String, RegExp, Array],
// 最大缓存数量
max: [String, Number],
},
include:字符串 / 正则 / 数组,只有名称匹配的组件会被缓存;exclude:字符串 / 正则 / 数组,名称匹配的组件不会被缓存(优先级高于 include);max:数字,限制缓存组件的最大数量,超出时按 LRU(最近最少使用)策略淘汰最久未使用的缓存组件。
include/exclude:匹配组件的 name 选项(或路由组件的 name),缓存前会校验是否符合规则;
max:结合 keys(Set 类型)实现 LRU,超出 max 时删除 keys 中第一个元素(最久未使用),并从 cache 中删除对应组件。
注意事项
- 只能有一个直接子组件。
生命周期钩子
缓存的组件不再执行常规的 mounted、unmounted 等(onMounted 只在首次挂载执行一次)。取而代之的是新增两个生命周期钩子:
onActivated:组件被激活(从缓存切入视图)时调用。首次插入 DOM 时也会触发。onDeactivated:组件被停用(从视图切出进入缓存)时调用。
示例 keep-alive 基本使用
<template>
<div accesskey="c">
<h2>这里是 KeepAlive 缓存组件</h2>
<button @click="handleClick">切换组件</button>
<keep-alive max="10" :include="['KeepA', 'KeepB']">
<component :is="comp ? KeepA : KeepB" />
</keep-alive>
</div>
</template>
<script setup lang="ts">
import KeepA from "@/pages/cloud/components/KeepA.vue";
import KeepB from "@/pages/cloud/components/KeepB.vue";
import { ref } from "vue";
const comp = ref(false);
const handleClick = () => {
comp.value = !comp.value;
};
</script>
const _hoisted_1 = { accesskey: "c" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", _hoisted_1, [
_cache[0] || (_cache[0] = _createElementVNode(
"h2",
null,
"这里是 KeepAlive 缓存组件",
-1
/* CACHED */
)),
_createElementVNode("button", { onClick: $setup.handleClick }, "切换组件"),
(_openBlock(), _createBlock(
_KeepAlive,
{
max: "10",
include: ["KeepA", "KeepB"]
},
[(_openBlock(), _createBlock(_resolveDynamicComponent($setup.comp ? $setup.KeepA : $setup.KeepB)))],
1024
/* DYNAMIC_SLOTS */
))
]);
}
KeepA 组件
<template>
<div accesskey="a">
<h2>这里是KeepA</h2>
</div>
</template>
<script setup lang="ts">
import { onActivated, onDeactivated, onMounted, onUpdated } from "vue";
onActivated(() => {
console.log("KeepA activated");
});
onDeactivated(() => {
console.log("KeepA deactivated");
});
onMounted(() => {
console.log("KeepA mounted");
});
onUpdated(() => {
console.log("KeepA updated");
});
</script>
import { createElementVNode as _createElementVNode, openBlock as _openBlock, createElementBlock as _createElementBlock } from "/node_modules/.vite/deps/vue.js?v=ebcf97fa";
const _hoisted_1 = { accesskey: "a" };
function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) {
return _openBlock(), _createElementBlock("div", _hoisted_1, [..._cache[0] || (_cache[0] = [_createElementVNode(
"h2",
null,
"这里是KeepA",
-1
/* CACHED */
)])]);
}
- 在首次渲染时,
<KeepAlive>的render函数会获取其默认插槽的第一个子组件 VNode,经过有效性检查(多子节点警告、非组件节点跳过)和include/exclude过滤后,计算出该子组件的缓存key(优先使用 VNode 的key,否则使用组件类型)。 - 由于此时
cache中没有对应记录,因此会直接进入“未命中”分支:- 将
key加入keys集合(用于 LRU 淘汰),若设置了max且超出则淘汰最早缓存; - 同时设置临时变量
pendingCacheKey = key,等待子组件挂载完成后将其实例存入cache。
- 将
- 最后,该子组件 VNode 会被打上
shapeFlag |= 256标记,告诉渲染器它由KeepAlive管理,并返回该 VNode 交给后续渲染流程。
示例
<template>
<div>
<h2>这里是问题管理</h2>
<KeepAlive
key="keep-alive"
max="4"
:exclude="['KeepC']"
:include="['KeepA', 'KeepB']"
>
<component :is="components[tabId]" />
</KeepAlive>
<button @click="handleClick">切换组件</button>
</div>
</template>
<script setup lang="ts">
import { ref, shallowReadonly } from "vue";
import KeepA from "./components/KeepA.vue";
import KeepB from "./components/KeepB.vue";
import KeepC from "./components/KeepC.vue";
const components = shallowReadonly([null, KeepA, KeepB, KeepC]);
const tabId = ref<number>(1);
const handleClick = () => {
console.log(tabId.value);
if (tabId.value > 0 && tabId.value < 3) {
tabId.value++;
} else {
tabId.value = 1;
}
};
defineOptions({
name: "CloudBugsView",
});
</script>
KeepA 组件挂载
- 挂载后会将组件vnode 缓存 到 cache 集合中
点击按钮 切换组件 KeepA ---> KeepB
卸载组件 KeepA
- 将 mounted 挂载钩子、activated 钩子 标志已失效
- 将 keepA 组件的vnode 挂载到 隐藏存储容器中
- 在完成 KeepB 挂载后 执行 KeepA 组件的 deactivated 失活钩子
- 执行 KeepB 的 mounted、activated生命周期钩子
执行 keepAlive 的 setup 的返回渲染函数
再次 KeepA -> KeepB
- 获取 keepB 缓存的 vode,将缓存的
vnode.el、vnode.compenent赋给 keepB 新vode相应属性。 - 将 KeepA 组件vnode 挂载到真实容器中
keys 处理
KeepAlive 通过 max 属性控制缓存的最大个数,并采用 LRU(Least Recently Used,最近最少使用), 缓存策略来管理缓存。LRU 策略的核心思想是:当缓存满了需要淘汰时,优先淘汰那些“最久没有被访问过”的缓存实例。
源码
packages/runtime-core/src/components/KeepAlive.ts
KeepAlive 内部通过 Map(cache)存储缓存组件的 VNode 实例,通过 Set(keys)管理缓存 key,切换组件时优先从缓存读取,而非重新渲染。
const KeepAlive = (__COMPAT__
? // Vue2 兼容模式
/*@__PURE__*/ decorate(KeepAliveImpl)
: // 原生 Vue3 模式
KeepAliveImpl) as any as {
__isKeepAlive: true
new (): {
$props: VNodeProps & KeepAliveProps
$slots: {
default(): VNode[] // 仅支持 default 默认插槽
}
}
}
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
__isKeepAlive: true, // 标识 KeepAlive 组件
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number],
},
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 获取当前组件实例
const instance = getCurrentInstance()!
// 从实例的上下文获取 KeepAlive 上下文
const sharedContext = instance.ctx as KeepAliveContext
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
const cache: Cache = new Map()
// 缓存已激活组件的 key
const keys: Keys = new Set()
// 当前激活的组件实例(只记录组件或suspense节点)
let current: VNode | null = null
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
;(instance as any).__v_cache = cache
}
const parentSuspense = instance.suspense // 父组件的 Suspense 实例
const {
renderer: {
p: patch, // 渲染函数
m: move, // 移动组件实例到新位置
um: _unmount, // 卸载组件实例
o: { createElement }, // 创建元素节点
},
} = sharedContext
// 组件实例的隐藏容器节点(用于暂存失活组件实例)
const storageContainer = createElement('div')
watch(
() => [props.include, props.exclude],
([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
{ flush: 'post', deep: true },
)
// cache sub tree after render
// 缓存子树,用于后续激活
let pendingCacheKey: CacheKey | null = null
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type && cached.key === vnode.key) {
resetShapeFlag(vnode)
const da = vnode.component!.da
// 触发 deactivated 钩子(延迟执行,保证时机正确)
da && queuePostRenderEffect(da, suspense)
return
}
unmount(cached)
})
})
// 渲染逻辑(setup 返回的渲染函数)
return () => {
pendingCacheKey = null
// 若没有默认插槽内容,重置缓存相关状态
if (!slots.default) {
return (current = null)
}
const children = slots.default() // 取默认插槽的子节点
const rawVNode = children[0] // 取第一个子节点
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
// 非组件/Suspense节点:不缓存,直接返回该节点
// 如元素节点、文本节点等
} else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
// Vue3 的 KeepAlive 组件仅针对「组件节点(Component VNode)」和「Suspense 节点」 做缓存
let vnode = getInnerChild(rawVNode)
if (vnode.type === Comment) {
// 注释节点,不缓存
current = null
return vnode // 直接返回注释节点,不做缓存
}
// vnode.type:VNode 的 type 属性,对于组件 VNode,type 指向「组件构造函数 / 组件选项对象」
const comp = vnode.type as ConcreteComponent
const name = getComponentName(
isAsyncWrapper(vnode)
? // 异步组件的 vnode.type 会被 Vue 包装成一个特殊对象
(vnode.type as ComponentOptions).__asyncResolved || {}
: comp,
)
const { include, exclude, max } = props
// 不满足缓存条件:标记为「不应缓存」,并返回原始节点
if (
(include && (!name || !matches(include, name))) || // 不在include中
(exclude && name && matches(exclude, name)) // 在exclude中
) {
vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE // 标记为「不应缓存」
current = vnode // 记录当前组件,用于后续激活时对比
return rawVNode // 直接返回原始节点,不做缓存
}
// 生成缓存 key(优先用 vnode.key,否则用组件类型)
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
pendingCacheKey = key // 缓存
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
setTransitionHooks(vnode, vnode.transition!)
}
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
keys.delete(key) // 从旧键列表中移除
keys.add(key) // 添加到最新键列表
} else {
keys.add(key) // 新增Key到LRU集合
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value!)
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE // 标记为需要缓存
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
},
}
deactivate 生命周期钩子
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
invalidateMount(instance.m)
invalidateMount(instance.a)
/**
* 将组件从页面容器移动到隐藏容器(暂存)
* 无锚点,直接移到容器末尾
*/
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 异步渲染完成后,调用 deactivated 生命周期钩子
// 将失活逻辑加入「渲染后副作用队列」,保证在「组件 DOM 已从页面移除、渲染完成后」执行
/**
* 为什么延迟执行?
* 失活逻辑依赖 DOM 已移除的状态,需在渲染器完成 DOM 操作后执行,避免钩子内操作到仍在页面中的 DOM。
*/
queuePostRenderEffect(() => {
// 触发组件 deactivated 生命周期钩子
if (instance.da) {
invokeArrayFns(instance.da)
}
// 触发 vnode 的 onVnodeUnmounted 钩子(模拟卸载行为,实际未卸载)
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true // 标记组件为失活状态
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
// for e2e test
if (__DEV__ && __BROWSER__) {
;(instance as any).__keepAliveStorageContainer = storageContainer
}
}
将 KeepB 切换到 KeepA
move 函数
activate 生命周期钩子
sharedContext.activate = (
vnode, // 要激活的组件
container, // 目标容器(页面可见的 DOM 容器)
anchor, // 插入锚点(组件将插入到该节点之前)
namespace, // 命名空间
optimized, // 是否启用优化模式
) => {
const instance = vnode.component! // 获取组件实例
// 将组件从隐藏容器移动到目标容器(页面可见位置)
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// in case props have changed
// 补丁更新:处理激活时的 props 变化等
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
namespace,
vnode.slotScopeIds,
optimized,
)
// 处理缓存组件「激活(activate)」的核心逻辑
/**
* 为什么延迟执行?
* 激活逻辑依赖 DOM 已挂载的状态(比如 onVnodeMounted 钩子需要访问已插入页面的 DOM 节点),必须在渲染器完成 DOM 插入后执行。
*/
queuePostRenderEffect(() => {
instance.isDeactivated = false // 恢复组件激活状态标记
// 触发组件 activated 生命周期钩子
/**
* 为什么激活钩子是数组而非单个函数?
* instance.a(activated 钩子)本质是一个函数数组,而非单个函数。
* 在 Vue 中,可以在一个组件里多次注册同一个生命周期钩子
*/
if (instance.a) {
invokeArrayFns(instance.a)
}
// 模拟触发 VNode 的 onVnodeMounted 钩子
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}
onMounted、onUpdated 生命周期钩子
// 组件挂载时缓存子树
onMounted(cacheSubtree)
// 组件更新时缓存子树
onUpdated(cacheSubtree)
const cacheSubtree = () => {
// fix #1621, the pendingCacheKey could be 0
if (pendingCacheKey != null) {
// if KeepAlive child is a Suspense, it needs to be cached after Suspense resolves
// avoid caching vnode that not been mounted
// 若 KeepAlive 子节点是 Suspense → 需等 Suspense 解析完成后再缓存
if (isSuspense(instance.subTree.type)) {
// 把缓存操作加入「Suspense 解析完成后的渲染后队列」
queuePostRenderEffect(() => {
// 存入缓存:key → Suspense 内部的真实子组件 VNode
cache.set(pendingCacheKey!, getInnerChild(instance.subTree))
}, instance.subTree.suspense)
} else {
// 非 Suspense 节点 → 直接缓存子树的真实组件 VNode
cache.set(pendingCacheKey, getInnerChild(instance.subTree))
}
}
}
onBeforeUnmount 生命周期构子
在 KeepAlive 组件自身被卸载时,清理其管理的所有缓存组件,对当前激活的缓存组件仅触发失活钩子,对非激活的缓存组件直接执行卸载,避免内存泄漏。
// 组件卸载前,移除缓存中的子树
// onBeforeUnmount:Vue 生命周期钩子,在组件即将被卸载时执行(此时组件仍在 DOM 中,可访问实例 / 缓存)
onBeforeUnmount(() => {
// cache:当前 KeepAlive 实例的缓存容器(Map 类型,key -> 组件 VNode)
cache.forEach(cached => {
// subTree:KeepAlive 组件渲染的子树(即其默认插槽中的内容)
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
// 处理当前激活的缓存组件:仅触发失活钩子,不卸载
if (cached.type === vnode.type && cached.key === vnode.key) {
// current instance will be unmounted as part of keep-alive's unmount
resetShapeFlag(vnode) // 重置 VNode 的形状标记,避免后续渲染异常
// but invoke its deactivated hook here
const da = vnode.component!.da
// 触发 deactivated 钩子(延迟执行,保证时机正确)
da && queuePostRenderEffect(da, suspense)
return
}
// 处理非激活的缓存组件:直接执行卸载
unmount(cached)
})
})