Vue3 KeepAlive 组件

6 阅读12分钟

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

源码

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

  // Marker for special handling inside the renderer. We are not using a ===
  // check directly on KeepAlive in the renderer, because importing it directly
  // would prevent it from being tree-shaken.
  __isKeepAlive: true, // 标识 KeepAlive 组件

  props: {
    // 包含的组件
    include: [String, RegExp, Array],
    // 排除的组件
    exclude: [String, RegExp, Array],
    // 最大缓存数量
    max: [String, Number],
  },

  // KeepAlive 组件的 setup 函数
  // setup 函数的执行时机:每个 KeepAlive 组件实例被创建时,都会独立执行一次 setup 函数
  setup(props: KeepAliveProps, { slots }: SetupContext) {
    // 获取当前组件实例
    const instance = getCurrentInstance()!
    // KeepAlive communicates with the instantiated renderer via the
    // ctx where the renderer passes in its internals,
    // and the KeepAlive instance exposes activate/deactivate implementations.
    // The whole point of this is to avoid importing KeepAlive directly in the
    // renderer to facilitate tree-shaking.
    // 从实例的上下文获取 KeepAlive 上下文
    const sharedContext = instance.ctx as KeepAliveContext

    // if the internal renderer is not registered, it indicates that this is server-side rendering,
    // for KeepAlive, we just need to render its children
    // 服务器端渲染时,直接返回子组件
    if (__SSR__ && !sharedContext.renderer) {
      return () => {
        const children = slots.default && slots.default()
        return children && children.length === 1 ? children[0] : children
      }
    }

    /**
     * 组件内部定义 cache 、keys
     * 不同位置使用的 KeepAlive 组件是「独立实例」,各自拥有专属的 cache 和 keys,不会共用同一份内存
     */
    // 缓存已激活的组件实例
    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')

    // 激活组件实例
    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)
      }
    }

    // 停用组件实例
    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
      }
    }

    // 卸载组件实例
    function unmount(vnode: VNode) {
      // reset the shapeFlag so it can be properly unmounted
      resetShapeFlag(vnode)
      _unmount(vnode, instance, parentSuspense, true)
    }

    // 修剪缓存,根据 filter 函数过滤出需要保留的组件实例
    function pruneCache(filter: (name: string) => boolean) {
      cache.forEach((vnode, key) => {
        // for async components, name check should be based in its loaded
        // inner component if available
        const name = getComponentName(
          isAsyncWrapper(vnode)
            ? (vnode.type as ComponentOptions).__asyncResolved || {}
            : (vnode.type as ConcreteComponent),
        )
        if (name && !filter(name)) {
          pruneCacheEntry(key)
        }
      })
    }

    // 修剪缓存中的指定组件实例
    function pruneCacheEntry(key: CacheKey) {
      const cached = cache.get(key) as VNode
      // 情况1:缓存存在,且不是当前激活的节点 → 执行卸载
      if (cached && (!current || !isSameVNodeType(cached, current))) {
        unmount(cached)

        // 情况2:缓存存在,且是当前激活的节点 → 重置缓存标记
      } else if (current) {
        // current active instance should no longer be kept-alive.
        // we can't unmount it now but it might be later, so reset its flag now.
        resetShapeFlag(current)
      }
      cache.delete(key) // 从缓存容器中移除该 key
      keys.delete(key) // 从 LRU 集合中移除该 key
    }

    // 监听 include/exclude 变化,修剪缓存
    // prune cache on include/exclude prop change
    watch(
      () => [props.include, props.exclude],
      ([include, exclude]) => {
        // include变化 → 清理「不在新include中的组件
        include && pruneCache(name => matches(include, name))
        // exclude变化 → 清理「在新exclude中的组件
        exclude && pruneCache(name => !matches(exclude, name))
      },
      // prune post-render after `current` has been updated
      { flush: 'post', deep: true },
    )

    // cache sub tree after render
    // 缓存子树,用于后续激活
    let pendingCacheKey: CacheKey | null = null

    // 处理「组件子树缓存」
    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))
        }
      }
    }
    // 组件挂载时缓存子树
    onMounted(cacheSubtree)
    // 组件更新时缓存子树
    onUpdated(cacheSubtree)

    // 组件卸载前,移除缓存中的子树
    // onBeforeUnmount:Vue 生命周期钩子,在组件即将被卸载时执行(此时组件仍在 DOM 中,可访问实例 / 缓存)
    /**
     * 在 KeepAlive 组件自身被卸载时,清理其管理的所有缓存组件
     * 对「当前激活的缓存组件」仅触发失活钩子,对「非激活的缓存组件」直接执行卸载,避免内存泄漏。
     */
    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)
      })
    })

    // 渲染逻辑(setup 返回的渲染函数)
    return () => {
      pendingCacheKey = null

      // 若没有默认插槽内容,重置缓存相关状态
      if (!slots.default) {
        return (current = null)
      }

      const children = slots.default() // 取默认插槽的子节点
      const rawVNode = children[0] // 取第一个子节点

      // 多节点场景:不缓存,直接返回所有子节点
      // Vue3 官方的 KeepAlive 组件本身只支持包裹单个直接子节点,如果检测到多个直接子节点,会直接跳过缓存逻辑、按普通方式渲染
      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)
      // #6028 Suspense ssContent maybe a comment VNode, should avoid caching it
      if (vnode.type === Comment) {
        // 注释节点,不缓存
        current = null
        return vnode // 直接返回注释节点,不做缓存
      }

      // vnode.type:VNode 的 type 属性,对于组件 VNode,type 指向「组件构造函数 / 组件选项对象」
      const comp = vnode.type as ConcreteComponent

      // for async components, name check should be based in its loaded
      // inner component if available
      // 获取组件名(如组件的name选项)
      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中
      ) {
        // #11717
        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)

      // clone vnode if it's reused because we are going to mutate it
      if (vnode.el) {
        vnode = cloneVNode(vnode)
        if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
          rawVNode.ssContent = vnode
        }
      }
      // #1511 it's possible for the returned vnode to be cloned due to attr
      // fallthrough or scopeId, so the vnode here may not be the final vnode
      // that is mounted. Instead of caching it directly, we store the pending
      // key and cache `instance.subTree` (the normalized vnode) in
      // mounted/updated hooks.
      pendingCacheKey = key // 缓存

      // 复用缓存实例
      if (cachedVNode) {
        // copy over mounted state
        vnode.el = cachedVNode.el
        vnode.component = cachedVNode.component
        if (vnode.transition) {
          // recursively update transition hooks on subTree
          setTransitionHooks(vnode, vnode.transition!)
        }
        // avoid vnode being mounted as fresh
        // 标记为「已缓存」,避免重新挂载
        vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
        // make this key the freshest
        keys.delete(key) // 从旧键列表中移除
        keys.add(key) // 添加到最新键列表
      } else {
        keys.add(key) // 新增Key到LRU集合
        // 超出max限制:淘汰最久未使用的缓存
        // prune oldest entry
        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
      }
    }

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

应用

cn.vuejs.org/guide/built…

最后

GitHub:github.com/hannah-lin-…