Vue3.5 内置组件(KeepAlive、Suspense、Teleport) 与 异步组件

85 阅读20分钟

Suspense

Suspense 是 Vue 3 的一个内置组件,用于在组件树中协调异步依赖的处理。它不是处理单个组件的加载状态,而是能在组件树的上层,等待下层多个嵌套异步依赖项解析完成,并统一显示加载状态。

Suspense 的核心工作流程

image.png

Suspense 的三个核心状态:

  • 初始渲染:在内存中渲染 #default 插槽,同时收集异步依赖
  • Pending 状态(挂起) :存在未完成的异步依赖,显示 #fallback 内容
  • Resolved 状态(完成) :所有依赖已完成,显示 #default 的真实内容

如果初始渲染中没有遇到任何异步依赖,Suspense 会直接进入 Resolved 状态。

mountSuspense 的执行过程:

  1. 创建一个离屏(隐藏)DOM 容器,用于提前渲染 #default 内容(异步组件等)。
  2. 创建 SuspenseBoundary 实例,管理异步依赖、状态切换和 DOM 操作。
  3. 将 #default 内容挂载到隐藏容器中(此时用户不可见)。
  4. 检查是否有未解决的异步依赖:
    • 有依赖:挂载 #fallback 到可见容器,用户看到加载状态;触发 pending / fallback 事件。
    • 无依赖:立即 resolve,将隐藏容器中的 #default 内容(vnode.el 真实DOM)插入到可见容器,完成渲染。

Suspense 可以等待的异步依赖类型

Suspense 能够识别并等待以下两类异步依赖:

  1. 带有 async setup() 的组件:包括使用 <script setup> 并包含顶层 await 表达式的组件
  2. 异步组件:通过 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>

image.png

插槽

在 Vue 3 中,<Suspense> 组件通过两个具名插槽来管理加载状态:

  • #default:放置需要等待异步依赖(异步组件、async setup())的真实内容。
  • #fallback:放置异步依赖加载期间显示的“加载中”内容(也就是 loading 状态)。
  • 错误处理通过 onErrorCaptured 生命周期钩子实现,Suspense 本身不捕获错误如果 #default 包含多个顶级元素,Vue 会在开发环境下发出警告。

如果 #default 插槽中只有同步组件(没有异步依赖),#fallback 永远不会显示

Suspense 的核心机制是等待异步依赖解析。当它渲染 #default 插槽时:

  1. 同步组件会立即完成渲染,不会产生任何待处理的 Promise。
  2. 如果没有检测到任何异步依赖(如 defineAsyncComponent 或 async setup),Suspense 会直接进入 Resolved(完成)  状态。
  3. 此时 #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>

image.png

示例 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>

编译结果

image.png

Suspence组件

image.png

Suspence组件虚拟节点的suspense属性

image.png

suspense.pendingBranch(ssContent)

image.png

解析组件 AsyncComponent-A 即之前的uspense.pendingBranch(ssContent

image.png

异步组件的 parentSuspense

image.png

异步组件 mountComponent

image.png

异步组件 创建组件实例

image.png

异步组件。执行 setupComponent(初始化props、slots),执行 setupStatefulComponent

image.png

异步组件 AsyncComponentWrapper 组件 执行组件 setup 函数

image.png

执行 suspense.registerDep 注册收集依赖

instance是异步组件、suspense 是 Suspense组件

image.png

suspense组件 dep大于 0 触发事件 onPendingonFallback,挂载 Suspense组件的 vnode.ssFallback,设置 vnode.ssFallback 为 suspense 的活跃分支activeBranch

image.png

mountSuspense 处理完毕,

image.png

当异步组件加载完毕

image.png

image.png

instance是异步组件,instance.asyncResolved表明异步组件已解析,执行handleSetupResult

image.png

instance.render image.png

finishComponentSetup

image.png

image.png

setupRenderEffect 给组件更新创建副作用函数,执行一次 update,实现组件挂载

image.png

image.png

组件内部实例vode

image.png

image.png

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>

image.png

示例 详细配置对象

<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>

image.png

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)]);
}

异步组件加载

image.png

image.png

image.png

image.png

示例 模拟 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>

image.png

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 被忽略。

image.png

隐藏容器中挂载 默认内容

image.png

卸载旧 vnode(activeBranch 即 fallback node)

image.png

移动

  • 移动前:pendingBranch 为 异步组件、activeBranchfallback 组件
  • 移动完成: 挂载异步组件在实际DOM中。activeBranch 为异步组件内容 。pendingBranch 为null
情况二:suspensible: false
  • 异步组件会独立显示自己的 loadingComponent(或默认无加载提示)。
  • 不会通知外层的 <Suspense> 等待自身,因此 <Suspense> 的 fallback 不会被触发(因为对 Suspense 来说,该子组件已经不是一个“异步依赖”)。
  • 若 Suspense 内部还有其他异步依赖,fallback 仍会因那些依赖而显示;如果没有其他依赖,fallback 则不会出现。
  • 结果loadingComponent 独立工作,外部 fallback 不受影响(除非没有其他依赖)。

image.png

Suspense 组件 vode

image.png

vnode.suspence = createSuspenseBoundary()

image.png

将主容器(suspense的默认插槽)挂载到隐藏容器

image.png

解析 suspense.resolve

在所有异步依赖都已解析完成后,立即同步地将待解析分支(pendingBranch)切换为激活分支(activeBranch)

image.png

image.png

在 Suspense 解析完成后,将等待中的内容(pendingBranch)从隐藏容器移动到实际的 DOM 容器中。

image.png

image.png

组件的真正 vnode 在 vnode.component.subTree

image.png

image.png

将 loading 挂载到实际容器上

image.png

image.png

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>

image.png

编译结果

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>

image.png

编译结果

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>

出现错误

image.png

[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>

image.png

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 中删除对应组件。

注意事项

  1. 只能有一个直接子组件。

生命周期钩子

缓存的组件不再执行常规的 mountedunmounted 等(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 */
	)])]);
}
  1. 在首次渲染时,<KeepAlive> 的 render 函数会获取其默认插槽的第一个子组件 VNode,经过有效性检查(多子节点警告、非组件节点跳过)和 include/exclude 过滤后,计算出该子组件的缓存 key(优先使用 VNode 的 key,否则使用组件类型)。
  2. 由于此时 cache 中没有对应记录,因此会直接进入“未命中”分支:
    • 将 key 加入 keys 集合(用于 LRU 淘汰),若设置了 max 且超出则淘汰最早缓存;
    • 同时设置临时变量 pendingCacheKey = key,等待子组件挂载完成后将其实例存入 cache
  3. 最后,该子组件 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>

image.png

KeepA 组件挂载

  • 挂载后会将组件vnode 缓存 到 cache 集合中

image.png

点击按钮 切换组件 KeepA ---> KeepB

卸载组件 KeepA

  • 将 mounted 挂载钩子、activated 钩子 标志已失效
  • 将 keepA 组件的vnode 挂载到 隐藏存储容器中
  • 在完成 KeepB 挂载后 执行 KeepA 组件的 deactivated 失活钩子
  • 执行 KeepB 的 mounted、activated生命周期钩子

执行 keepAlive 的 setup 的返回渲染函数

image.png

image.png

再次 KeepA -> KeepB

  • 获取 keepB 缓存的 vode,将缓存的 vnode.elvnode.compenent赋给 keepB 新vode相应属性。
  • 将 KeepA 组件vnode 挂载到真实容器中

image.png

keys 处理

KeepAlive 通过 max 属性控制缓存的最大个数,并采用 LRU(Least Recently Used,最近最少使用),  缓存策略来管理缓存。LRU 策略的核心思想是:当缓存满了需要淘汰时,优先淘汰那些“最久没有被访问过”的缓存实例

image.png

image.png

image.png

源码

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
    }
  },
}

image.png

image.png

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

image.png

image.png

move 函数

image.png

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)
  })
})

最后

  1. keepAlive API