Vue3 源码解读-KeepAlive 组件实现原理


💡 [本系列Vue3源码解读文章基于3.3.4版本](https://github.com/vuejs/core/tree/v3.3.4) 欢迎关注公众号:《前端 Talkking》

1、前言

在 Vuejs 中,内置了 KeepAlive组件用于缓存组件,可以避免组件的销毁/重建,提高性能 。假设页面有一组 Tab组件,如下代码所示:

html 复制代码
<template>
  <Tab v-if="currentTab === 1">...</Tab>
  <Tab v-if="currentTab === 2">...</Tab>
  <Tab v-if="currentTab === 3">...</Tab>
</template>

可以看到,根据变量 currentTab的不同,会渲染不同的 Tab组件。当用户频繁的切换 Tab 时,会导致不停地卸载并重建对应的 Tab组件。为了避免因此产生的性能开销,可以使用 KeepAlive组件解决这个问题,如下代码所示:

html 复制代码
<template>
 <KeepAlive>
  <Tab v-if="currentTab === 1">...</Tab>
  <Tab v-if="currentTab === 2">...</Tab>
  <Tab v-if="currentTab === 3">...</Tab>
 </KeepAlive>
</template>

这样,无论用户怎么切换 Tab组件,都不会发生频繁的创建和销毁,因此会极大地优化对用户操作的响应,尤其在大组件场景下,优势会更加明显。

2、KeepAlive 组件-源码实现

2.1 原理

KeepAlive组件的本质是缓存管理以及特殊的挂载/卸载逻辑。被 KeepAlive包裹的组件在卸载的时候并不是真正的卸载,而是将该组件搬运到一个隐藏的容器中,实现假卸载,从而使得组件可以维持当前状态。而当挂载的时候,会讲它从隐藏容器中搬运到原容器。

KeepAlive组件提供了 activatedeactivate两个生命周期函数来实现挂载卸载的逻辑,如下图所示:

typescript 复制代码
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // KeepAlive独有标识
  __isKeepAlive: true,

  props: {
    // 配置了该属性,那么只有名称匹配的组件会被缓存
    include: [String, RegExp, Array],
    // 配置了该属性,那么任何名称匹配的组件都不会被缓存
    exclude: [String, RegExp, Array],
    // 最多可以缓存多少组件实例
    max: [String, Number]
  },
  setup(props: KeepAliveProps, { slots }: SetupContext) {

    // 省略部分代码

    // 返回一个函数,该函数将会直接作为组件的render函数
    return () => {

      // 省略部分代码

    }
  }
}

从以上代码中我们可以得知,KeepAlive组件上有 name__isKeepAlivepropssetup 等属性,它们的作用分别是:

  • nameKeepAlive 组件名称;
  • _isKeepAliveKeepAlive 组件标识;
  • propsKeepAlive 组件属性;
  • setupKeepAlive 组件渲染 render函数。

2.2 挂载-activate

在挂载组件的时候会调用 processComponent函数,其源码实现如下:

processComponent 源码实现

typescript 复制代码
const processComponent = (
    n1: VNode | null,
    n2: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    parentComponent: ComponentInternalInstance | null,
    parentSuspense: SuspenseBoundary | null,
    isSVG: boolean,
    slotScopeIds: string[] | null,
    optimized: boolean
  ) => {
    n2.slotScopeIds = slotScopeIds
    // 挂载组件
    if (n1 == null) {
      // 判断当前要挂载的组件是否是KeepAlive组件
      if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
        // 激活组件,即将隐藏容器中移动到原容器中
        ;(parentComponent!.ctx as KeepAliveContext).activate(
          n2,
          container,
          anchor,
          isSVG,
          optimized
        )
      } else {
        // 不是KeepAlive组件,调用mountComponent挂载组件
        mountComponent(
          n2,
          container,
          anchor,
          parentComponent,
          parentSuspense,
          isSVG,
          optimized
        )
      }
    } else {
      // 更新组件
      updateComponent(n1, n2, optimized)
    }
  }

在该函数中,挂载的时候首先判断了 shapeFlag的值,如果挂载的组件是 KeepAlive组件,则调用 activate函数激活组件,否则调用 mountComponent函数挂载组件。KeepAlive组件中 activate源码实现如下:

activate 函数源码实现

typescript 复制代码
// 该函数用于激活组件
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  // 组件实例
  const instance = vnode.component!
        // 将组件从隐藏容器中移动到原容器中(即页面中)
        move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  // in case props have changed
  // props可能会发生变化,因此需要执行patch过程
  patch(
    instance.vnode,
    vnode,
    container,
    anchor,
    instance,
    parentSuspense,
    isSVG,
    vnode.slotScopeIds,
    optimized
  )
  // 异步渲染
  queuePostRenderEffect(() => {
    instance.isDeactivated = false
    if (instance.a) {
      invokeArrayFns(instance.a)
    }
    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)
  }
}

根据上面的源码我们得知,KeepAlive组件挂载时候是调用 move方法,将组件从隐藏容器中移动到原页面中。由于重新挂载时,props可能会发生变化,因此需要重新执行 patch过程。

2.3 卸载-deactivate

unmount 函数源码实现

卸载组件的时候会调用 unmount方法,在该方法中判断了 shapeFlag的值,如果卸载的组件是 KeepAlive组件,则调用 deactivate方法将组件搬运到隐藏容器中,然后直接返回,否则执行的是卸载组件的逻辑,将组件真正的卸载掉。

typescript 复制代码
const unmount: UnmountFn = (
    vnode,
    parentComponent,
    parentSuspense,
    doRemove = false,
    optimized = false
  ) => {
    const {
      type,
      props,
      ref,
      children,
      dynamicChildren,
      shapeFlag,
      patchFlag,
      dirs
    } = vnode
    // unset ref
    if (ref != null) {
      setRef(ref, null, parentSuspense, vnode, true)
    }
    // 判断当前要挂载的组件是否是KeepAlive组件
    if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
      // 调用KeepAlive组件的deactivate方法使组件失活,即将组件搬运到一个隐藏的容器中
      ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
      return
    }

    // 其他卸载组件的处理逻辑
  }

接着我们来看deactive 函数源码实现

typescript 复制代码
 // 将组件移动到隐藏容器中
    sharedContext.deactivate = (vnode: VNode) => {
      // 组件实例
      const instance = vnode.component!
      // 将组件移动到隐藏容器中
      move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
      // 异步渲染
      queuePostRenderEffect(() => {
        if (instance.da) {
          invokeArrayFns(instance.da)
        }
        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)
      }
    }

在该方法中,调用 move方法,将组件搬运到一个隐藏容器中。

2.4 include 和 exclude

默认情况下,KeepAlive组件会对所有的"内部组件"进行缓存,为了给用户提供自定义的缓存规则,KeepAlive组件提供了 includeexclude这两个 props,用户可以自定义哪些组件需要被 KeepAlive,哪些组件不需要被 KeepAlive

KeepAlive组件的 props 定义如下:

typescript 复制代码
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // KeepAlive独有标识
  __isKeepAlive: true,

  props: {
    // 配置了该属性,那么只有名称匹配的组件会被缓存
    include: [String, RegExp, Array],
    // 配置了该属性,那么任何名称匹配的组件都不会被缓存
    exclude: [String, RegExp, Array],
    // 最多可以缓存多少组件实例
    max: [String, Number]
  },
}

KeepAlive组件挂载时,它会根据"内部组件"的名称(即 name选项)进行匹配,如下代码所示:

typescript 复制代码
const KeepAliveImpl: ComponentOptions = {
  name: `KeepAlive`,
  // KeepAlive独有标识
  __isKeepAlive: true,

  props: {
    // 配置了该属性,那么只有名称匹配的组件会被缓存
    include: [String, RegExp, Array],
    // 配置了该属性,那么任何名称匹配的组件都不会被缓存
    exclude: [String, RegExp, Array],
    // 最多可以缓存多少组件实例
    max: [String, Number]
  },
  setup(props: KeepAliveProps, { slots }: SetupContext) {

    // 省略部分代码

    // 返回一个函数,该函数将会直接作为组件的render函数
    return () => {

      // 省略部分代码

      // 获取用户传递的include、exclude、max
      const { include, exclude, max } = props
      // 如果name没有被include匹配或者被exclude匹配
      if (
        (include && (!name || !matches(include, name))) ||
        (exclude && name && matches(exclude, name))
      ) {
        // 则直接渲染内部组件,不对其进行后续的缓存操作,将当前渲染的属性存储到current上
        current = vnode
        return rawVNode
      }

      // 省略部分代码

      current = vnode
      return isSuspense(rawVNode.type) ? rawVNode : vnode
    }
  }
}

function matches(pattern: MatchPattern, name: string): boolean {
  if (isArray(pattern)) {
    // 如果是数组,则遍历pattern,递归调用matches,判断是否包含当前组件
    return pattern.some((p: string | RegExp) => matches(p, name))
  } else if (isString(pattern)) {
    // 如果是字符串,则分割字符串,判断pattern是否包含当前组件
    return pattern.split(',').includes(name)
  } else if (isRegExp(pattern)) {
    // 如果是正则,则使用正则匹配判断是否包含当前组件
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

根据上面的源码,会根据用户传入的 includeexclude对"内部组件"名称匹配,如果 name没有被匹配到则直接渲染"内部组件",否则需要缓存组件。

2.5 缓存管理

假设有如下模版内容:

Html 复制代码
<keep-alive>
  <h1 v-if="flag">h1</h1>
  <h2 v-else>h2</h2>
</keep-alive>

借助Vue SFC Playground平台,编译后的代码如下:

javascript 复制代码
import { unref as _unref, openBlock as _openBlock, createElementBlock as _createElementBlock, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, createBlock as _createBlock } from "vue"

const _hoisted_1 = { key: 0 }
const _hoisted_2 = { key: 1 }

import { ref } from 'vue'


const __sfc__ = {
  __name: 'App',
  setup(__props) {

const msg = ref('Hello World!')
let flag = true

return (_ctx, _cache) => {
  return (_openBlock(), _createBlock(_KeepAlive, null, [
    (_unref(flag))
      ? (_openBlock(), _createElementBlock("h1", _hoisted_1, "h1"))
      : (_openBlock(), _createElementBlock("h2", _hoisted_2, "h2"))
  ], 1024 /* DYNAMIC_SLOTS */))
}
}

}

根据编译后的代码可知,KeepAlive的子节点创建的时候都添加了一个 key_hoisted_1_hoisted_2)。

然后渲染 KeepAlive组件的时候会对缓存做一定的处理,如下所示:

typescript 复制代码
  // 返回一个函数,该函数将会直接作为组件的render函数
return () => {
  // 省略部分代码

  // 根据vnode的key去缓存中查找是否有缓存的组件
  const cachedVNode = cache.get(key)

  // clone vnode if it's reused because we are going to mutate it
  if (vnode.el) {
    // 复制vnode为了复用
    vnode = cloneVNode(vnode)
    if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
      rawVNode.ssContent = vnode
    }
  }
  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
                         // 将shapeFlag设置为COMPONENT_KEPT_ALIVE,vnode避免挂载为新的
                         vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
                         // make this key the freshest
                         keys.delete(key)
      keys.add(key)
    } else {
      keys.add(key)
      // prune oldest entry
      // 当缓存梳理超过指定阈值时对缓存进行修剪
      if (max && keys.size > parseInt(max as string, 10)) {
        pruneCacheEntry(keys.values().next().value)
      }
    }
    // 省略部分代码
  }
}

以上代码是 Vue.js 中处理 KeepAlive组件缓存逻辑的一部分,它允许组件保持状态或避免重新渲染。

  • 如果 cachedVNode 存在,表示有缓存的虚拟节点:

    • 将缓存的 DOM 元素 el 赋值给当前虚拟节点 vnode.el,这样就可以复用 DOM 元素,而不是重新创建。
    • 将缓存的组件实例 component 赋值给当前虚拟节点 vnode.component,这样组件状态可以保持不变。
    • 如果 vnode 有过渡效果,递归地更新子树的过渡钩子函数。
    • 通过设置 vnode.shapeFlagCOMPONENT_KEPT_ALIVE,避免将 vnode 当作新组件挂载。
    • 更新缓存键 key 的位置,将其从当前位置删除后重新添加到 keys 集合的末尾,这样可以保持 key 是最新的。
  • 如果 cachedVNode 不存在,表示没有缓存的虚拟节点:

    • 将新的 key 添加到 keys 集合中。
    • 如果缓存的大小超过了设定的最大值 max,则从缓存中删除最旧的条目。这是通过 pruneCacheEntry 函数实现的,它接收最旧条目的 key 并执行删除操作(LRU缓存策略)。

这段代码的目的是优化组件的渲染性能,通过复用组件实例和 DOM 元素来避免不必要的渲染开销。同时,它也管理着缓存的大小,确保不会因为缓存过多而消耗过多的内存。

KeepAlive组件挂载后会执行 onMounted生命周期函数,组件更新会执行 onUpdated生命周期函数,设置对应 key的组件缓存。

KeepAlive 设置缓存源码实现

typescript 复制代码
// cache sub tree after render
let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
  // fix #1621, the pendingCacheKey could be 0
  if (pendingCacheKey != null) {
    // 设置缓存
    cache.set(pendingCacheKey, getInnerChild(instance.subTree))
  }
}
// 执行生命周期函数
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

3、总结

KeepAlive 的本质作用是缓存组件,总结如下:

  • 卸载不是真正的卸载,是把组件移动到一个隐藏容器中,挂载是从隐藏容器中搬运到原页面中,以提高组件卸载和挂载的性能;
  • 用户可以指定 includeexclude来指定哪些组件可以被缓存,哪些组件不可以被缓存;
  • 缓存策略采用的 LRU策略。

4、参考资料

1\][vue官网](https://link.juejin.cn?target=https%3A%2F%2Fcn.vuejs.org%2F "https://cn.vuejs.org/") \[2\][vuejs设计与实现](https://link.juejin.cn?target=https%3A%2F%2Fwww.ituring.com.cn%2Fbook%2F2953 "https://www.ituring.com.cn/book/2953") \[3\][vue3源码](https://link.juejin.cn?target=https%3A%2F%2Fgithub.com%2Fvuejs%2Fcore%2Fblob%2Fv3.3.4 "https://github.com/vuejs/core/blob/v3.3.4")

相关推荐
崔庆才丨静觅6 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60617 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了7 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅7 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅8 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅8 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment8 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅8 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊8 小时前
jwt介绍
前端
爱敲代码的小鱼8 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax