vue3源码解析:KeepAlive组件实现

6 阅读6分钟

1. 组件示例

让我们先看一个典型的 KeepAlive 使用场景:

// 1. 定义两个组件
// TabA.vue
<template>
  <div>
    <span>Count: {{ count }}</span>
    <button @click="count++">Add</button>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  const count = ref(0)
</script>

// TabB.vue
<template>
  <input v-model="message" />
</template>

<script setup>
  import { ref } from 'vue'
  const message = ref('')
</script>

// App.vue
<template>
  <div>
    <div>
      <button @click="currentTab = 'TabA'">Tab A</button>
      <button @click="currentTab = 'TabB'">Tab B</button>
    </div>

    <keep-alive :max="2" :include="['TabA', 'TabB']">
      <component :is="currentTab === 'TabA' ? TabA : TabB" />
    </keep-alive>
  </div>
</template>

<script setup>
  import { ref } from 'vue'
  import TabA from './TabA.vue'
  import TabB from './TabB.vue'

  const currentTab = ref('TabA')
</script>

这个示例展示了:

  1. 两个需要缓存的组件:TabA(计数器)和 TabB(输入框)

  2. KeepAlive 的基本配置:

    • max: 最大缓存数量
    • include: 需要缓存的组件名
  3. 使用 component 动态组件实现切换

  4. 组件状态(count 和 message)在切换时会被保持

2. 组件数据结构

KeepAlive 组件的核心数据结构:

// 匹配模式类型:字符串、正则或它们的数组
type MatchPattern = string | RegExp | (string | RegExp)[]

// 缓存键类型
type CacheKey = PropertyKey | ConcreteComponent;
// 缓存存储
type Cache = Map<CacheKey, VNode>;
// 缓存键集合
type Keys = Set<CacheKey>;

// 组件属性
interface KeepAliveProps {
  include?: MatchPattern; // 包含的组件
  exclude?: MatchPattern; // 排除的组件
  max?: number | string; // 最大缓存数
}

// KeepAlive 上下文接口
interface KeepAliveContext extends ComponentRenderContext {
  renderer: RendererInternals;
  activate: (
    vnode: VNode,
    container: RendererElement,
    anchor: RendererNode | null,
    namespace: ElementNamespace,
    optimized: boolean
  ) => void;
  deactivate: (vnode: VNode) => void;
}

// 组件定义
const KeepAliveImpl: ComponentOptions = {
  name: 'KeepAlive',

  // 用于渲染器中特殊处理的标记
  __isKeepAlive: true,

  props: {
    include: [String, RegExp, Array],
    exclude: [String, RegExp, Array],
    max: [String, Number]
  }
}

// 导出的公共类型
export const KeepAlive = {
  __isKeepAlive: true,
  new(): {
    $props: VNodeProps & KeepAliveProps;
    $slots: {
      default(): VNode[];
    };
  };
}

关键数据结构说明:

  1. 匹配相关:

    • MatchPattern:用于 include/exclude 的匹配模式
    • matches 函数:支持字符串、正则和数组的匹配
  2. 缓存相关:

    • Cache:使用 Map 存储组件 VNode
    • Keys:使用 Set 实现 LRU 缓存策略
    • CacheKey:支持组件类型或自定义 key
  3. 组件通信:

    • KeepAliveContext:与渲染器通信的接口
    • activate/deactivate:组件激活和停用的方法
  4. 类型标记:

    • __isKeepAlive:用于渲染器识别 KeepAlive 组件
    • ShapeFlags:用于标记组件的缓存状态

3. 实现原理分析

3.1 初始化阶段

当 KeepAlive 组件被创建时,setup 函数首先进行初始化:

setup(props: KeepAliveProps, { slots }: SetupContext) {
  const instance = getCurrentInstance()!
  const sharedContext = instance.ctx as KeepAliveContext

  // 1. 创建缓存存储
  const cache: Cache = new Map()
  const keys: Keys = new Set()
  let current: VNode | null = null
  let pendingCacheKey: CacheKey | null = null

  // 2. 获取渲染器方法
  const {
    renderer: {
      p: patch,
      m: move,
      um: _unmount,
      o: { createElement }
    }
  } = sharedContext

  // 3. 创建隐藏容器
  const storageContainer = createElement('div')

  // 4. 定义卸载方法
  const unmount = (vnode: VNode) => {
    // 重置 keep alive 标记
    resetShapeFlag(vnode)
    _unmount(vnode, instance, parentSuspense, true)
  }

  // 5. 定义缓存清理方法
  function pruneCache(filter?: (name: string) => boolean) {
    cache.forEach((vnode, key) => {
      const name = getComponentName(vnode.type as ConcreteComponent)
      if (name && (!filter || !filter(name))) {
        pruneCacheEntry(key)
      }
    })
  }

  // 6. 处理 Suspense 组件
  if (__SSR__ && !sharedContext.renderer) {
    return () => {
      const children = slots.default && slots.default()
      return children && children.length === 1 ? children[0] : children
    }
  }

  // 7. 注册缓存更新钩子
  onMounted(cacheSubtree)
  onUpdated(cacheSubtree)

  // 8. 注册卸载钩子
  onBeforeUnmount(() => {
    cache.forEach(cached => {
      unmount(cached)
    })
  })

  // 9. 监听 include/exclude 变化
  watch(
    () => [props.include, props.exclude],
    ([include, exclude]) => {
      include && pruneCache(name => matches(include, name))
      exclude && pruneCache(name => !matches(exclude, name))
    },
    { flush: 'post', deep: true }
  )

  // ... 后续实现
}

setup 函数的主要职责:

  1. 初始化缓存相关的数据结构和状态
  2. 获取渲染器方法,创建存储容器
  3. 定义组件缓存和清理的核心方法
  4. 处理服务端渲染的特殊情况
  5. 注册生命周期钩子和监听器
  6. 返回渲染函数

3.2 组件首次渲染

以上面的示例为例,当首次渲染 TabA 时:

return () => {
  // 重置缓存键
  pendingCacheKey = null;

  // 1. 获取要渲染的组件
  if (!slots.default) {
    return null;
  }
  const children = slots.default();
  const rawVNode = children[0];

  // 检查是否只有一个子组件
  if (children.length > 1) {
    if (__DEV__) {
      warn(`KeepAlive should contain exactly one component child.`);
    }
    current = null;
    return children;
  }

  // 检查是否是有效的组件
  if (
    !isVNode(rawVNode) ||
    (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
      !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
  ) {
    current = null;
    return rawVNode;
  }

  // 2. 获取组件信息
  let vnode = getInnerChild(rawVNode);

  // 处理 Comment 类型
  if (vnode.type === Comment) {
    current = null;
    return vnode;
  }

  const comp = vnode.type; // TabA 组件定义
  const name = getComponentName(
    isAsyncWrapper(vnode)
      ? (vnode.type as ComponentOptions).__asyncResolved || {}
      : comp
  );

  // 3. 检查是否需要缓存
  const { include, exclude, max } = props;
  if (
    (include && !matches(include, name)) ||
    (exclude && matches(exclude, name))
  ) {
    vnode.shapeFlag &= ~ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;
    current = vnode;
    return rawVNode;
  }

  // 4. 生成缓存键并缓存组件
  const key = vnode.key == null ? comp : vnode.key;
  const cachedVNode = cache.get(key);

  // 处理 vnode 复用的情况
  if (vnode.el) {
    vnode = cloneVNode(vnode);
    if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
      rawVNode.ssContent = vnode;
    }
  }

  pendingCacheKey = key;

  // 5. 处理缓存命中的情况
  if (cachedVNode) {
    // 复制已挂载的状态
    vnode.el = cachedVNode.el;
    vnode.component = cachedVNode.component;

    // 更新过渡钩子
    if (vnode.transition) {
      setTransitionHooks(vnode, vnode.transition!);
    }

    // 标记为已缓存组件
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE;

    // 更新 LRU 顺序
    keys.delete(key);
    keys.add(key);
  } else {
    // 新组件:添加到缓存
    keys.add(key);

    // 超出缓存上限时清理最旧的组件
    if (max && keys.size > parseInt(max as string, 10)) {
      pruneCacheEntry(keys.values().next().value!);
    }
  }

  // 标记为 keep-alive 组件
  vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE;

  // 更新当前组件引用
  current = vnode;
  return isSuspense(rawVNode.type) ? rawVNode : vnode;
};

首次渲染的主要流程:

  1. 渲染前准备:

    • 重置缓存键
    • 获取默认插槽内容
    • 验证子组件数量和类型
  2. 组件处理:

    • 获取实际渲染的组件 vnode
    • 处理特殊类型(Comment、Suspense)
    • 获取组件名称用于匹配
  3. 缓存判断:

    • 检查 include/exclude 配置
    • 决定是否需要缓存该组件
    • 生成缓存键
  4. 状态处理:

    • 处理组件复用情况
    • 克隆 vnode 避免状态污染
    • 设置缓存标记
  5. 缓存更新:

    • 处理缓存命中的情况
    • 维护 LRU 缓存顺序
    • 控制缓存数量上限

3.3 组件切换过程

当切换到 TabB 时:

// 1. TabA 停用
sharedContext.deactivate = (vnode: VNode) => {
  const instance = vnode.component!;

  // 移动到隐藏容器
  move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense);

  // 触发停用钩子
  queuePostRenderEffect(() => {
    instance.isDeactivated = true;
    if (instance.da) {
      invokeArrayFns(instance.da);
    }
  }, parentSuspense);
};

// 2. TabB 激活
sharedContext.activate = (vnode, container, anchor) => {
  const instance = vnode.component!;

  // 移动到目标容器
  move(vnode, container, anchor, MoveType.ENTER, parentSuspense);

  // 更新属性
  patch(instance.vnode, vnode, container, anchor, instance, parentSuspense);

  // 触发激活钩子
  queuePostRenderEffect(() => {
    instance.isDeactivated = false;
    if (instance.a) {
      invokeArrayFns(instance.a);
    }
  }, parentSuspense);
};

组件切换的处理流程:

  1. 停用当前组件:

    • 将组件移动到隐藏容器
    • 标记组件为停用状态
    • 触发 deactivated 钩子
    • 处理相关的 vnode 钩子
  2. 激活新组件:

    • 移动到目标容器位置
    • 更新组件属性和状态
    • 触发 activated 钩子
    • 更新组件树状态
  3. 优化处理:

    • 使用移动而不是销毁重建
    • 保持 DOM 结构完整性
    • 维护组件实例状态

3.4 缓存更新和清理

// 1. 缓存组件
function cacheSubtree() {
  if (pendingCacheKey != null) {
    cache.set(pendingCacheKey, getInnerChild(instance.subTree));
  }
}

// 在组件挂载和更新时缓存
onMounted(cacheSubtree);
onUpdated(cacheSubtree);

// 2. 清理缓存
function pruneCacheEntry(key: CacheKey) {
  const cached = cache.get(key) as VNode;
  if (!current || !isSameVNodeType(cached, current)) {
    unmount(cached);
  }
  cache.delete(key);
  keys.delete(key);
}

// 3. 监听 include/exclude 变化
watch(
  () => [props.include, props.exclude],
  ([include, exclude]) => {
    include && pruneCache((name) => matches(include, name));
    exclude && pruneCache((name) => !matches(exclude, name));
  },
  { flush: "post", deep: true }
);

缓存管理的关键流程:

  1. 缓存更新:

    • 在组件挂载时缓存
    • 在组件更新时更新缓存
    • 使用 pendingCacheKey 追踪待缓存组件
  2. 缓存清理:

    • 根据 include/exclude 变化清理
    • 超出 max 限制时清理最旧组件
    • 卸载被清理的组件实例
  3. 缓存优化:

    • 使用 Set 维护 LRU 顺序
    • 动态响应配置变化
    • 智能处理缓存更新时机

3.5 组件卸载

onBeforeUnmount(() => {
  cache.forEach((cached) => {
    unmount(cached);
  });
});

卸载阶段的处理流程:

  1. 清理准备:

    • 在组件卸载前触发
    • 遍历所有缓存的组件
  2. 资源释放:

    • 重置组件的 keepAlive 标记
    • 执行组件的完整卸载流程
    • 清理相关的 DOM 节点
  3. 收尾工作:

    • 触发必要的生命周期钩子
    • 确保所有缓存组件被正确清理
    • 防止内存泄漏

4. 总结

KeepAlive 组件通过以下机制实现组件状态缓存:

  1. 缓存机制:

    • 使用 Map 存储组件 vnode
    • 使用 Set 维护 LRU 缓存顺序
    • 支持最大缓存数限制
  2. DOM 处理:

    • 使用隐藏容器存储非活动组件
    • 通过 move 操作切换组件
    • 保持 DOM 结构完整
  3. 生命周期:

    • 提供 activated/deactivated 钩子
    • 在合适的时机触发钩子函数
    • 支持嵌套组件的生命周期管理
  4. 性能优化:

    • 避免重复创建组件实例
    • 减少不必要的 DOM 操作
    • 智能的缓存清理策略