异步组件、Suspense、Teleport 与 KeepAlive 的实现机制
第17章:异步组件、Suspense、Teleport 与 KeepAlive 的实现机制
<KeepAlive>不是"暂停"组件,而是将组件的 subTree DOM 节点物理移入一个独立的离线容器(document.createElement('div')),组件实例本身一直存活,只是从视口消失了——这就是为什么缓存的组件可以在零毫秒内"恢复",而不需要重新执行 setup() 或挂载 DOM。
本章核心问题:Vue 的四个内置"特殊组件"(异步组件、Suspense、Teleport、KeepAlive)都有自己专属的渲染逻辑,它们是如何绕过标准的组件挂载流程工作的?
读完本章你将理解:
- 异步组件内部状态机的完整结构(加载/错误/超时/重试)
- Suspense 如何捕获后代组件抛出的 Promise 并控制 fallback 显示
- Teleport 是如何在 VNode 层和 DOM 层分别维护两套不同的树结构的
- KeepAlive 的 LRU 缓存策略和"搬家"操作的实现原理
- 四个特殊组件组合使用时的陷阱
Level 1 · 你需要知道的(1-3年经验)
四个特殊组件的本质
| 组件 | 本质 | 解决的问题 |
|---|---|---|
| 异步组件 | 一个状态机包装器 | 懒加载组件定义,控制加载/错误 UI |
| Suspense | 一个异步边界控制器 | 协调多个异步依赖,统一 fallback 策略 |
| Teleport | 一个 DOM 位置路由器 | 把子节点渲染到 DOM 树的任意位置 |
| KeepAlive | 一个组件缓存管理器 | 保存组件状态,避免重复 mount/unmount |
defineAsyncComponent 的基本用法
// 最简形式
const AsyncComp = defineAsyncComponent(() => import('./MyComp.vue'))
// 完整配置
const AsyncComp = defineAsyncComponent({
loader: () => import('./MyComp.vue'),
loadingComponent: LoadingSpinner, // 加载中显示
errorComponent: ErrorDisplay, // 加载失败显示
delay: 200, // 延迟 200ms 后才显示 loading(避免闪烁)
timeout: 3000, // 3秒超时
onError(error, retry, fail, attempts) {
if (attempts < 3) retry() // 自动重试 3 次
else fail()
}
})
Suspense 的使用场景
<template>
<Suspense>
<!-- 默认插槽:实际内容(可以包含异步组件或 async setup) -->
<template #default>
<UserProfile /> <!-- async setup: await fetch('/api/user') -->
<PostList /> <!-- async setup: await fetch('/api/posts') -->
</template>
<!-- fallback 插槽:所有异步依赖 resolve 前显示 -->
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
注意:Suspense 等所有后代的异步依赖都 resolve 后,才切换到 default 内容。不是"第一个 resolve 就显示"。
Teleport:把内容渲染到"别处"
<!-- 问题场景:Modal 被嵌套在有 overflow:hidden 或 z-index 的容器里 -->
<div class="card" style="overflow: hidden">
<button @click="showModal = true">打开弹窗</button>
<!-- 解决:Teleport 把 Modal 的 DOM 插入 body,绕过 overflow 限制 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<p>这个弹窗在 body 下,不受 overflow 限制</p>
</div>
</Teleport>
</div>
KeepAlive:缓存组件状态
<template>
<KeepAlive
:include="['UserList', 'ProductList']"
:exclude="['LoginForm']"
:max="10"
>
<component :is="currentPage" />
</KeepAlive>
</template>
<script setup>
// 被缓存的组件可以监听这两个生命周期
onActivated(() => {
console.log('组件被激活(从缓存中恢复)')
// 适合在这里刷新数据
refreshData()
})
onDeactivated(() => {
console.log('组件被停用(进入缓存)')
// 适合在这里清理定时器
clearTimers()
})
</script>
Level 2 · 它是怎么运行的(3-5年经验)
defineAsyncComponent 的内部状态机
异步组件状态机:
┌─────────────────────────────────────────────────────────┐
│ 异步组件的生命周期 │
│ │
│ PENDING ──────────────────────────────► RESOLVED │
│ │ │ │
│ │ loader() 失败 │ 渲染实 │
│ ▼ ▼ 际组件 │
│ ERROR ◄── timeout 超时 延迟到期后 │
│ │ 显示 loading │
│ │ onError() 调用 retry() │
│ └──────────────────────────► 重新进入 PENDING │
│ │
│ delay=200ms: PENDING 期间等待 200ms 再显示 loading │
│ timeout=3s: 3秒后如果还是 PENDING → ERROR │
└─────────────────────────────────────────────────────────┘
// packages/runtime-core/src/apiAsyncComponent.ts(简化)
export function defineAsyncComponent(source) {
if (isFunction(source)) {
source = { loader: source }
}
const {
loader,
loadingComponent,
errorComponent,
delay = 200,
timeout,
onError: userOnError,
} = source
let resolvedComp: ConcreteComponent | undefined
// 重试函数,闭包持有重试次数
let retries = 0
const retry = () => {
retries++
pendingRequest = null
return load()
}
const load = () => {
let thisRequest: Promise<ConcreteComponent>
return (
pendingRequest ||
(pendingRequest = thisRequest = loader()
.catch(err => {
err = err instanceof Error ? err : new Error(String(err))
if (userOnError) {
return new Promise((resolve, reject) => {
const userRetry = () => resolve(retry())
const userFail = () => reject(err)
userOnError(err, userRetry, userFail, retries + 1)
})
} else {
throw err
}
})
.then((comp: any) => {
// 支持 ES Module:{ default: Comp }
if (comp && (comp.__esModule || comp[Symbol.toStringTag] === 'Module')) {
comp = comp.default
}
resolvedComp = comp
return comp
}))
)
}
// 返回一个"包装组件",它实际上是一个有状态组件
return defineComponent({
name: 'AsyncComponentWrapper',
__asyncLoader: load,
setup() {
const instance = getCurrentInstance()!
// 如果已经加载完成(SSR或预加载),直接返回
if (resolvedComp) {
return () => createInnerComp(resolvedComp!, instance)
}
// 错误处理
const onError = (err: Error) => {
pendingRequest = null
handleError(err, instance, ErrorCodes.ASYNC_COMPONENT_LOADER)
}
// Suspense 支持:如果在 Suspense 内,抛出 Promise
if (suspensible && instance.suspense) {
return load()
.then(() => () => createInnerComp(resolvedComp!, instance))
.catch(err => {
onError(err)
return () => errorComponent ? createVNode(errorComponent, { error: err }) : null
})
}
// 非 Suspense 模式:使用响应式状态控制显示
const loaded = ref(false)
const error = ref<Error | undefined>()
const delayed = ref(!!delay) // 延迟显示 loading
if (delay) {
setTimeout(() => { delayed.value = false }, delay)
}
if (timeout != null) {
setTimeout(() => {
if (!loaded.value && !error.value) {
const err = new Error(`Async component timed out after ${timeout}ms.`)
onError(err)
error.value = err
}
}, timeout)
}
load()
.then(() => { loaded.value = true })
.catch(err => { onError(err); error.value = err })
return () => {
if (loaded.value && resolvedComp) {
return createInnerComp(resolvedComp, instance)
} else if (error.value && errorComponent) {
return createVNode(errorComponent, { error: error.value })
} else if (!delayed.value && loadingComponent) {
return createVNode(loadingComponent)
}
}
},
})
}
Suspense 的内部实现原理
Suspense 的双分支结构:
┌─────────────────────────────────────────────────────────────┐
│ Suspense 内部维护两个分支: │
│ │
│ pendingBranch: 正在加载的内容(default slot VNode) │
│ activeBranch: 当前显示的内容(初始为 fallback) │
│ │
│ 挂载过程: │
│ 1. 设置 pendingBranch = default slot VNode │
│ 2. 挂载 pendingBranch(触发所有后代的异步 setup) │
│ 3. 每当一个异步依赖 resolve,pendingDeps-- │
│ 4. 当 pendingDeps === 0: │
│ ├── 将 pendingBranch 移入 DOM(替换 fallback) │
│ ├── 设置 activeBranch = pendingBranch │
│ └── 清空 pendingBranch │
│ │
│ "挂载但不插入 DOM" 的技巧: │
│ pendingBranch 在一个离线 container 中挂载 │
│ 等所有异步完成后,才把这个 container 的内容移入真实 DOM │
└─────────────────────────────────────────────────────────────┘
// packages/runtime-core/src/components/Suspense.ts(关键部分简化)
const SuspenseImpl = {
process(n1, n2, container, anchor, parentComponent, ...) {
if (n1 == null) {
mountSuspense(n2, container, anchor, ...)
} else {
patchSuspense(n1, n2, container, anchor, ...)
}
}
}
function mountSuspense(vnode, container, anchor, ...) {
const suspense = (vnode.suspense = createSuspenseBoundary(vnode, ...))
// 挂载 fallback(立即显示)
suspense.activeBranch = normalizeSuspenseSlot(vnode.ssContent)
patch(null, suspense.activeBranch, container, anchor, ...)
// 在离线容器中挂载 pendingBranch(异步内容)
const pendingBranch = (suspense.pendingBranch = normalizeSuspenseSlot(vnode.ssFallback))
patch(null, pendingBranch, suspense.hiddenContainer, null, ...)
// 注:hiddenContainer = document.createElement('div')
// 如果挂载过程中有异步组件抛出了 Promise
// suspense.deps 会大于 0,等待所有 Promise resolve
}
// 当一个异步依赖 resolve 时调用
function resolveSuspense(suspense) {
suspense.pendingDeps--
if (suspense.pendingDeps === 0) {
// 所有异步依赖都 resolve 了
const { vnode, activeBranch, pendingBranch, container } = suspense
// 卸载旧的 activeBranch(fallback)
unmount(activeBranch, ...)
// 把 pendingBranch 从离线容器移入真实 DOM
move(pendingBranch, container, ...)
suspense.activeBranch = pendingBranch
suspense.pendingBranch = null
// 触发 onResolve 回调
queuePostFlushCb(suspense.effects)
}
}
Teleport 的两层树结构
Teleport 的 VNode 树 vs DOM 树:
VNode 树(逻辑结构,与模板对应):
App
└── Card(有 overflow:hidden)
└── Teleport(to="body")
└── Modal
真实 DOM 树(物理结构):
<body>
<div id="app">
<div class="card"> ← Card 的 DOM
<!-- Teleport 在 VNode 树中的位置 -->
<!-- 但它的子节点 Modal 不在这里 -->
</div>
</div>
<div class="modal"> ← Modal 的 DOM(Teleport 的 to="body" 效果)
</div>
</body>
渲染器维护两套引用:
Teleport VNode.el → Card 内部的锚点文本节点
Teleport VNode.target → body 元素(to 属性指向的目标)
// packages/runtime-core/src/components/Teleport.ts(简化)
const TeleportImpl = {
__isTeleport: true,
process(n1, n2, container, anchor, parentComponent, ...) {
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = (n2.targetAnchor = hostCreateText(''))
hostInsert(targetAnchor, target!) // 在目标容器中插入锚点
if (n1 == null) {
// 挂载:把子节点插入 target(不是 container!)
mountChildren(n2.children, target!, targetAnchor, ...)
} else {
// 更新:处理 to 属性变化(可能需要移动到新 target)
const { shapeFlag, children, props } = n2
if (n2.target !== n1.target) {
// target 改变了,移动所有子节点到新 target
const nextTarget = (n2.target = resolveTarget(n2.props, querySelector))
children.forEach(c => move(c, nextTarget!, ...))
} else {
patchChildren(n1, n2, target!, targetAnchor, ...)
}
}
},
// 移除 Teleport 时,从 target 中移除子节点
remove(vnode, ...) {
const { shapeFlag, children, target, targetAnchor } = vnode
removeFragmentNodes(children, target!, ...)
hostRemove(targetAnchor!)
},
// 服务端渲染支持
hydrate(vnode, ...) { ... }
}
KeepAlive 的 LRU 缓存实现
KeepAlive LRU 缓存结构:
max = 3 时的缓存状态演示:
初始:cache = {},keys = []
访问 A:cache = {A: instanceA},keys = [A]
访问 B:cache = {A:..., B:...},keys = [A, B]
访问 C:cache = {..., C:...},keys = [A, B, C] ← 缓存满
访问 A(已缓存):
keys 中删除 A,重新追加到末尾
keys = [B, C, A](A 是最近使用的)
访问 D(新组件,缓存满):
需要删除 LRU(最久未使用)= keys[0] = B
卸载 B 的组件实例,从 cache 删除 B
cache = {C:..., A:..., D:...},keys = [C, A, D]
// packages/runtime-core/src/components/KeepAlive.ts(简化)
const KeepAliveImpl = defineComponent({
name: 'KeepAlive',
__isKeepAlive: true,
setup(props, { slots }) {
const instance = getCurrentInstance()!
const ctx = instance.ctx as KeepAliveContext
// 离线容器:缓存的组件 DOM 存放在这里
const storageContainer = createElement('div')
// 注入 move 和 unmount 方法到渲染器上下文
// 供渲染器在 mountComponent 时调用
ctx.activate = (vnode, container, anchor, ...) => {
// 从 storageContainer 移入 container(O(1) DOM 操作!)
move(vnode, container, anchor, MoveType.ENTER, ...)
// 触发 onActivated 钩子
queuePostFlushCb(invokeOnActivated)
}
ctx.deactivate = (vnode) => {
// 从当前容器移入 storageContainer(O(1) DOM 操作!)
move(vnode, storageContainer, null, MoveType.LEAVE, ...)
// 触发 onDeactivated 钩子
queuePostFlushCb(invokeOnDeactivated)
}
let pendingCacheKey: CacheKey | null = null
const cache: Cache = new Map()
const keys: Keys = new Set()
// 修剪缓存:删除 LRU 项
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached) // 真正卸载:调用 onBeforeUnmount/onUnmounted
} else if (current) {
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
return () => {
const children = slots.default?.()
if (!children?.length) return null
const rawVNode = children[0]
// 只缓存组件类型的 VNode
if (!isVNode(rawVNode) || !(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT)) {
return rawVNode
}
let vnode = rawVNode
const comp = vnode.type as ConcreteComponent
const name = getComponentName(comp)
// 检查 include/exclude 过滤
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
return vnode
}
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
if (cachedVNode) {
// 缓存命中!复用组件实例
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 打上 COMPONENT_KEPT_ALIVE 标记:告诉渲染器不要重新挂载
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// 更新 LRU 顺序
keys.delete(key)
keys.add(key)
} else {
// 缓存未命中:首次挂载
keys.add(key)
// LRU:如果超出 max,删除最老的
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
}
pendingCacheKey = key
cache.set(key, vnode)
return vnode
}
}
})
Level 3 · 设计文档与源码(资深开发者)
Suspense 如何捕获后代的 Promise
异步组件在 setup() 中返回一个 Promise(而不是返回对象或渲染函数)时,渲染器会识别这种情况:
// packages/runtime-core/src/renderer.ts
function setupStatefulComponent(instance) {
const { setup } = instance.type
const setupResult = callWithErrorHandling(setup, instance, ...)
if (isPromise(setupResult)) {
// setup() 返回了 Promise(async setup 或异步组件)
setupResult.then(unsetCurrentInstance, unsetCurrentInstance)
if (instance.suspense) {
// 有 Suspense 祖先:向上冒泡 Promise
instance.suspense.registerDep(instance, setupResult)
} else {
// 没有 Suspense:使用异步组件的包装器处理
instance.asyncDep = setupResult
}
return
}
// ... 正常处理
}
Suspense.registerDep 的核心逻辑:
function createSuspenseBoundary(vnode, ...) {
return {
// ...
registerDep(instance, setupPromise) {
suspense.pendingDeps++ // 计数器+1
setupPromise.then(asyncSetupResult => {
// 异步 setup 完成,处理返回值
instance.asyncResolved = true
finishComponent(instance, asyncSetupResult)
suspense.pendingDeps-- // 计数器-1
if (suspense.pendingDeps === 0) {
// 所有异步依赖都完成了
resolveSuspense(suspense)
}
}).catch(err => {
// 某个异步依赖失败了
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
suspense.pendingDeps--
})
}
}
}
Teleport 的 disabled 属性实现
// Teleport 支持 disabled 属性:disabled 时,子节点渲染到原位(不 teleport)
const TeleportImpl = {
process(n1, n2, container, anchor, ...) {
const disabled = isTeleportDisabled(n2.props)
if (disabled) {
// disabled 模式:直接挂载到 container(标准行为)
mountChildren(n2.children, container, anchor, ...)
} else {
const target = resolveTarget(n2.props, querySelector)
mountChildren(n2.children, target, targetAnchor, ...)
}
}
}
// 常见用法:根据屏幕尺寸决定是否 teleport
const isMobile = ref(window.innerWidth < 768)
// <Teleport to="body" :disabled="!isMobile">
// 桌面端:渲染在原位;移动端:teleport 到 body
KeepAlive 的 COMPONENT_KEPT_ALIVE 标记与渲染器协作
渲染器在 mountComponent 时会检查 COMPONENT_KEPT_ALIVE 标记:
// packages/runtime-core/src/renderer.ts
const processComponent = (n1, n2, container, anchor, ...) => {
if (n1 == null) {
// 首次挂载:检查是否是从 KeepAlive 恢复
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
// 从缓存恢复:调用 KeepAlive 的 activate 方法
// 这会把 DOM 从 storageContainer 移入 container
parentComponent.ctx.activate(n2, container, anchor, ...)
} else {
// 正常首次挂载
mountComponent(n2, container, anchor, ...)
}
} else {
updateComponent(n1, n2, ...)
}
}
// 当 KeepAlive 的子组件被切换出去时(不是 unmount,而是 deactivate)
const unmountComponent = (instance, ...) => {
// 检查是否应该进入 KeepAlive 缓存
if (instance.shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
// 不真正卸载,而是移到离线容器
instance.ctx.deactivate(instance.vnode)
return
}
// 否则正常卸载...
}
Suspense 的错误捕获机制
Suspense 和 onErrorCaptured 是两套独立的机制:
// onErrorCaptured 处理组件内的同步错误和某些异步错误
// Suspense 专门处理 setup() 返回的 Promise 被 reject 的情况
// 如果 Suspense 内的组件在 setup() 的 Promise 中 reject:
setupPromise.catch(err => {
// 向上传播到 Suspense 边界的错误处理
handleError(err, instance, ErrorCodes.SETUP_FUNCTION)
// 如果有 errorComponent,显示 errorComponent
// 如果没有,继续向上冒泡到 onErrorCaptured
})
// 组合使用(推荐模式):
// 1. Suspense 的 fallback 处理加载态
// 2. 外层包裹 onErrorCaptured 处理错误态
const MyPage = defineComponent({
errorCaptured(err) {
showErrorToast(err.message)
return false // 阻止向上传播
},
template: `
<Suspense>
<template #default><AsyncContent /></template>
<template #fallback><Skeleton /></template>
</Suspense>
`
})
Level 4 · 边界与陷阱(全体适用)
陷阱1:KeepAlive 缓存的是组件 VNode,不是 DOM
// 常见误解:以为 KeepAlive 把 DOM 节点缓存起来
// 实际:KeepAlive 缓存的是带有 component 实例引用的 VNode
// DOM 节点存放在 storageContainer 中(一个离线的 div)
// 这意味着:
// 1. 被缓存的组件仍然占用内存(组件实例 + DOM 节点)
// 2. max 属性控制的是缓存的组件数量上限
// 3. 超出 max 时,最久未使用的组件会被真正卸载(调用 onBeforeUnmount)
// 监控 KeepAlive 内存:
const cache = getCurrentInstance()?.ctx?.cache // 不建议直接访问,仅供调试
陷阱2:Teleport 的目标元素必须在 mount 时已存在于 DOM
// 错误:to 指向的元素还没有被渲染
const app = createApp(App)
app.mount('#app')
// 此时 #modal-root 可能还不存在!
// 正确做法1:确保目标元素在 mount 之前就在 HTML 中
// <div id="modal-root"></div> ← 写在 HTML 文件里
// 正确做法2:动态目标
const teleportTarget = ref('#app') // 先 teleport 到 #app
onMounted(() => {
// mount 后动态创建目标
const el = document.createElement('div')
el.id = 'modal-root'
document.body.appendChild(el)
teleportTarget.value = '#modal-root'
})
陷阱3:KeepAlive 内的 v-if 会导致组件被真正卸载
<template>
<KeepAlive>
<!-- 错误:v-if 为 false 时,组件被 unmount,KeepAlive 无效! -->
<MyComp v-if="show" />
<!-- 正确:用 v-show 控制显示,组件始终存在于 KeepAlive 中 -->
<MyComp v-show="show" />
<!-- 或者:用 include/exclude 控制哪些组件被缓存 -->
<!-- <component :is="show ? MyComp : OtherComp" /> -->
</KeepAlive>
</template>
陷阱4:Suspense 嵌套时的错误捕获行为
<!-- 嵌套 Suspense 的错误处理行为不直观 -->
<Suspense>
<template #default>
<Suspense> <!-- 内层 Suspense -->
<template #default>
<AsyncComp /> <!-- 如果这里报错 -->
</template>
<template #fallback>内层 loading</template>
</Suspense>
</template>
<template #fallback>外层 loading</template>
</Suspense>
<!--
AsyncComp 的 setup Promise 被 reject 时:
内层 Suspense 会先捕获这个错误
如果内层 Suspense 没有 errorComponent,
错误会向上传播到外层 Suspense 或 onErrorCaptured
这个行为在 Vue 3.3 之前不稳定,确保版本 >= 3.3
-->
陷阱5:Teleport + KeepAlive 组合的事件监听器泄漏
<template>
<KeepAlive>
<component :is="currentPage" />
</KeepAlive>
<!-- 问题:如果 currentPage 内有 Teleport,Teleport 的目标 DOM 在 deactivate 时不会被清理 -->
<!-- Teleport 的子节点会留在 body 中,即使组件"进入缓存" -->
</template>
<!-- 解决:在 onDeactivated 中手动清理 Teleport 产生的副作用 -->
<script setup>
onDeactivated(() => {
// 清理 Teleport 相关的事件监听器
document.removeEventListener('click', globalClickHandler)
})
</script>
陷阱6:defineAsyncComponent 的 delay 与 timeout 单位混淆
const AsyncComp = defineAsyncComponent({
loader: () => import('./Heavy.vue'),
delay: 200, // 单位:毫秒。等待 200ms 后才显示 loadingComponent
timeout: 3000, // 单位:毫秒。3000ms 后如果还没加载完,显示 errorComponent
// 常见错误:误以为 delay 是秒数
// delay: 2 不是 2 秒,而是 2 毫秒(几乎立即显示 loading)
// 常见错误:timeout 设置过短
// timeout: 300 在 3G 网络下几乎必然超时
})
本章小结
-
异步组件是一个状态机包装器:
defineAsyncComponent返回的不是原始组件,而是一个有状态的包装组件,内部管理 PENDING/RESOLVED/ERROR 三种状态;delay和timeout是两个独立的定时器,分别控制 loading 出现时机和超时检测。 -
Suspense 通过 pendingDeps 计数器协调多个异步依赖:后代组件的 async setup 返回 Promise 时,Suspense 拦截这个 Promise 并将计数器加一;所有 Promise resolve 后,才把离线 container 中的内容移入真实 DOM,替换 fallback。
-
Teleport 维护两套独立的树:在 VNode 树中,Teleport 的子节点仍然是 Teleport VNode 的逻辑子节点(用于 diff);在 DOM 树中,子节点被物理插入到
to属性指向的目标元素下,与 VNode 树的逻辑结构完全分离。 -
KeepAlive 的"缓存"是把 DOM 搬家而非复制:被停用的组件,其 DOM 子树被物理移入一个离线的 div(storageContainer),激活时再移回来,整个过程是 O(1) 的 DOM 操作,不重新执行 setup();
max属性触发的 LRU 淘汰会真正调用 onBeforeUnmount/onUnmounted。 -
四个特殊组件都绕过了标准的挂载流程:它们各自实现了
process/remove/move/hydrate方法,渲染器通过 shapeFlag(TELEPORT/SUSPENSE)或特殊标记(COMPONENT_KEPT_ALIVE)识别并转发给这些方法,而不走通用的mountElement/mountComponent路径。