第 17 章

异步组件、Suspense、Teleport 与 KeepAlive 的实现机制

第17章:异步组件、Suspense、Teleport 与 KeepAlive 的实现机制

<KeepAlive> 不是"暂停"组件,而是将组件的 subTree DOM 节点物理移入一个独立的离线容器(document.createElement('div')),组件实例本身一直存活,只是从视口消失了——这就是为什么缓存的组件可以在零毫秒内"恢复",而不需要重新执行 setup() 或挂载 DOM。

本章核心问题:Vue 的四个内置"特殊组件"(异步组件、Suspense、Teleport、KeepAlive)都有自己专属的渲染逻辑,它们是如何绕过标准的组件挂载流程工作的?

读完本章你将理解

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 网络下几乎必然超时
})

本章小结

  1. 异步组件是一个状态机包装器defineAsyncComponent 返回的不是原始组件,而是一个有状态的包装组件,内部管理 PENDING/RESOLVED/ERROR 三种状态;delaytimeout 是两个独立的定时器,分别控制 loading 出现时机和超时检测。

  2. Suspense 通过 pendingDeps 计数器协调多个异步依赖:后代组件的 async setup 返回 Promise 时,Suspense 拦截这个 Promise 并将计数器加一;所有 Promise resolve 后,才把离线 container 中的内容移入真实 DOM,替换 fallback。

  3. Teleport 维护两套独立的树:在 VNode 树中,Teleport 的子节点仍然是 Teleport VNode 的逻辑子节点(用于 diff);在 DOM 树中,子节点被物理插入到 to 属性指向的目标元素下,与 VNode 树的逻辑结构完全分离。

  4. KeepAlive 的"缓存"是把 DOM 搬家而非复制:被停用的组件,其 DOM 子树被物理移入一个离线的 div(storageContainer),激活时再移回来,整个过程是 O(1) 的 DOM 操作,不重新执行 setup();max 属性触发的 LRU 淘汰会真正调用 onBeforeUnmount/onUnmounted。

  5. 四个特殊组件都绕过了标准的挂载流程:它们各自实现了 process/remove/move/hydrate 方法,渲染器通过 shapeFlag(TELEPORT/SUSPENSE)或特殊标记(COMPONENT_KEPT_ALIVE)识别并转发给这些方法,而不走通用的 mountElement/mountComponent 路径。

本章评分
4.9  / 5  (14 评分)

💬 留言讨论