除了 <KeepAlive>、<component> 这些高频内置组件,Vue 3 还提供了两个解决特定场景痛点的组件:<Teleport> 和 <Suspense>。它们分别解决了“组件渲染到 DOM 的什么位置”和“异步组件加载时应该如何展示”这两个问题。
Teleport:把内容“传送”到任意 DOM 节点
在组件化开发中,每个组件的模板最终都会被渲染到该组件在 DOM 树中所处的位置。但有些 UI 元素(如模态框、全局通知、下拉菜单)在逻辑上属于当前组件,在视觉上却需要“脱离”当前 DOM 层级,挂载到更外层(甚至 <body> 下),以避免被父容器的 CSS(overflow: hidden、z-index、transform)裁剪或影响定位。
Vue 2 时代通常需要手动用 appendChild 将元素移到 body 下,破坏组件之间的独立性和响应式绑定。Vue 3 的 <Teleport> 内置组件则优雅地解决了这个问题:你可以在模板中把内容写在组件内部,但渲染时它会“瞬移”到你指定的 DOM 节点下,同时保持与当前组件的响应式连接、事件通信完全正常。
基础用法:
<template>
<div>
<button @click="show = true">打开弹窗</button>
<!-- to 指定传送目标,支持 CSS 选择器 -->
<Teleport to="body">
<div v-if="show" class="modal">
<p>这是一个模态框,但渲染在 body 下</p>
<button @click="show = false">关闭</button>
</div>
</Teleport>
</div>
</template>
<script setup>
import { ref } from 'vue'
const show = ref(false)
</script>
当 show 为 true 时,<div class="modal"> 会被实际挂载到 <body> 标签的末尾,而不是当前组件的 <div> 内部。这个模态框里面的 @click 事件、响应式数据 (show) 仍然正常工作,完全感觉不到 DOM 位置的“跨越”。
多个传送门到同一目标:
<Teleport to="#modals">
<div>A</div>
</Teleport>
<Teleport to="#modals">
<div>B</div>
</Teleport>
<!-- 会按先后顺序追加到 #modals 内部 -->
条件禁用传送:
某些场景(如 SSR 或移动端调试)下可能希望临时让内容回归原位,可以使用 disabled 属性动态控制:
<Teleport to="body" :disabled="isMobile">
<div class="tooltip">...</div>
</Teleport>
当 isMobile 为 true 时,内容渲染在当前组件的正常位置,而不是 body 下。
常见使用场景:
- 模态框 (Modal)/对话框:避免被
overflow: hidden父容器裁剪 - 通知/全局提示 (Toast/Notification):统一挂载到
body方便层级管理 - 下拉菜单/弹出层 (Dropdown/Popover):在复杂布局中防止定位错乱
- 移动端全屏遮罩:需要覆盖整个视口,必须脱离局部容器
注意点:
- Teleport 只是改变 DOM 挂载位置,不会改变组件间的父子关系——依然可以用
props、emits、provide/inject通信。 - 挂载目标必须在 Teleport 组件挂载前就已存在。如果目标是某个组件内的节点,注意渲染时序。
- 多个 Teleport 到同一目标时,内容的先后顺序由组件的挂载顺序决定。
Suspense:优雅处理异步依赖的加载态
组件在渲染时,如果它自身或它的子组件是异步组件(比如 defineAsyncComponent 或路由懒加载引入的组件),就会产生一个“空白期”——组件还没加载完,用户可能看到白屏或布局抖动。<Suspense> 的作用就是在这段等待时间内展示一个统一的骨架屏或加载状态,加载完成后再展示真实内容。
基础用法:
<template>
<Suspense>
<!-- 默认插槽:异步加载完成后展示的内容 -->
<template #default>
<AsyncDashboard />
</template>
<!-- fallback 插槽:加载中展示的内容 -->
<template #fallback>
<div class="loading-skeleton">加载中...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncDashboard = defineAsyncComponent(() =>
import('./components/Dashboard.vue')
)
</script>
当 AsyncDashboard 还在网络请求或代码解析过程中,用户会看到 <div class="loading-skeleton">加载中...</div>;异步组件加载完成并可以渲染时,Vue 会自动切换到 <AsyncDashboard />。切换是平滑的,不会有布局闪烁。
深入一点的用法:处理多个异步依赖
<Suspense> 不仅能处理异步组件,还能处理具有 async setup() 的组合式 API 组件(即组件本身可能返回 Promise)。如果 <Suspense> 内部有多个异步子组件,它会等待所有异步子组件全部就绪后,才一次性展示默认内容,避免分批出现导致的布局跳动。
<template>
<Suspense>
<template #default>
<AsyncHeader />
<AsyncContent />
</template>
<template #fallback>
<AppLoading />
</template>
</Suspense>
</template>
事件支持:<Suspense> 提供了三个生命周期事件,方便对加载状态做更细粒度的控制(如埋点、加载超时处理):
<Suspense
@resolve="onResolve" <!-- 所有异步加载完成 -->
@pending="onPending" <!-- 开始进入加载状态 -->
@fallback="onFallback" <!-- CPU 阻塞或其他原因降级到 fallback -->
>
...
</Suspense>
结合错误边界:
异步组件可能加载失败(如网络问题),建议配合 onErrorCaptured 或 <component> 的错误处理来显示错误提示,防止整个树崩溃。
使用场景与注意事项:
适用场景:
- 路由级别:在
<RouterView>外层包裹<Suspense>,为所有路由切换提供统一的加载过渡。 - 组件懒加载:仪表盘、图表这类重型组件或第三方依赖,首次加载时间长,用骨架屏替代白屏。
- 依赖异步数据(实验性):组件自身需要异步获取数据后才能渲染(但更推荐用
onMounted处理数据请求,<Suspense>主要用于组件代码的加载)。
注意事项:
<Suspense>目前还处于实验性阶段,API 相对稳定但在小型迭代中仍需留意破坏性变更(官方文档有标记)。- 它不能替代应用级别的数据加载状态(如接口请求的 loading)。
<Suspense>主要关注的是组件代码的加载和具有异步 setup 的组件,而不是普通数据请求的处理。对于数据加载的过渡效果,建议使用v-if+ loading 状态变量,或者TanStack Query等工具。 - 服务端渲染(SSR)下,
<Suspense>的行为略有不同,Nuxt 3 已经对其做了良好集成,可按 Nuxt 文档直接使用。
小结
<Teleport> 解决了“组件逻辑归属与 DOM 挂载位置分离”的难题,让模态框、通知等 UI 元素的实现变得自然且符合组件化哲学;<Suspense> 则为异步组件的加载等待提供了声明式的骨架屏方案,避免用户面对未知的空白或闪烁。它们并不需要每个页面都用到,但在特定场景下,可以让代码干净一大截。