likes
comments
collection
share

Vue中KeepAlive 原理与源码分析

作者站长头像
站长
· 阅读数 9

Vue中KeepAlive 原理与源码分析

概念

keep-alive是Vue的一个内置实例,用于缓存组件。当keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是摧毁它们。keep-alive 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链之中。如需要在两个组件之间来回切换,或者从一个分类页跳转到详情页,之后再返回分类页,仍然可以保持上次的状态,又或者在浏览首页时重新切换回来,滚动条会保持在上次位置(等等),这时候我们就可以用到keep-alive组件。

作用

  • 在组件切换的过程中将状态保留在内存。
    
  • 防止重复渲染DOM,减少加载时间以及性能的消耗,提高用户的体验感。
    

Props(参数)

  • include - string | RegExp | Array。只有名称匹配的组件会被缓存。
  • exclude - string | RegExp | Array。任何名称匹配的组件都不会被缓存。
  • max - number | string。最多可以缓存多少组件实例。

生命周期函数

activated : 在组件激活的时候使用,第一次激活是在mounted之后执行。

deactivated : 在失效的时候使用。

注意 :

  • 被包含在keep-alive中创建的组件,会多出两个生命周期的钩子:activated和deactivated
  • 使用keep-alive会将数据保留在内存中,如果要在每次进入页面的时候获取最新数据,需要在activated阶段获取数据,承担原来created钩子函数中获取数据的任务。

用法

  •  keep-alive的使用只需要在动态组件的最外层添加标签即可。
在动态组件中的应用
<!-- 基本 -->
<keep-alive>
  <component :is="choose"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
  <comp-a v-if="a > 1"></comp-a>
  <comp-b v-else></comp-b>
</keep-alive>
在vue-router中的应用
<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
 <router-view></router-view>
</keep-alive>

实现原理

步骤解析

Vue中KeepAlive 原理与源码分析 keep-alive 的本质是缓存管理和特殊的挂载/卸载逻辑。keep-alive 组件的实现需要渲染器层面的支持。这是因为被 keep-alive 的组件在卸载的时候,渲染器并不会真的将其卸载,而是会将该组件搬运到一个隐藏的容器中,实现 “假卸载”,从而使得组件可以维持当前状态。当被 keep-alive 的组件再次被 “挂载” 时,渲染器也不会真的挂载它,而是将它从隐藏容器中搬运到原容器。

通过 keep-alive 组件插槽,获取第一个子节点。根据 includeexclude 判断是否需要缓存,通过组件的 key,判断是否命中缓存。利用 LRU 算法,更新缓存以及对应的 keys 数组。根据 max 控制缓存的最大组件数量。

源码分析

基本结构

export declare const KeepAlive: {
    new (): {
        $props: VNodeProps & KeepAliveProps;
    };
    __isKeepAlive: true;
};

export declare type VNodeProps = {
    key?: string | number | symbol;
    ref?: VNodeRef;
    ref_for?: boolean;
    ref_key?: string;
    onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[];
    onVnodeMounted?: VNodeMountHook | VNodeMountHook[];
    onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[];
    onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[];
    onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[];
    onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[];
};

export declare interface KeepAliveProps {
    include?: MatchPattern;
    exclude?: MatchPattern;
    max?: number | string;
}


declare type MatchPattern = string | RegExp | (string | RegExp)[];

主要流程

在 keep-alive 组件的实现中,使用了 Map 对象和 Set 对象来实现组件的缓存,如下代码所示:

// 创建一个缓存对象
const cache: Cache = new Map()
// 存储组件的 key
const keys: Keys = new Set()

其中 Map 对象的键是组件选项对象,即 vnode.type 属性的值,而该 Map 对象的值是用于描述组件的 vnode 对象。由于用于描述组件的 vnode 对象存在对组件实例的引用 (即 vnode.component属性),所以缓存 vnode 对象,就等价与缓存了组件实例。

Set 对象存储的是组件对象的 keySet 对象具有自动去重的功能,因此Set对象中存储重复的组件对象。

keep-alive 组件中对缓存的管理时,首先会在组件的 onMountedonUpdated 生命周期钩子函数中设置缓存,如下代码所示:

let pendingCacheKey: CacheKey | null = null
const cacheSubtree = () => {
  if (pendingCacheKey != null) {
    // 设置缓存
    cache.set(pendingCacheKey, getInnerChild(instance.subTree))
  }
}
// 执行生命周期钩子函数
onMounted(cacheSubtree)
onUpdated(cacheSubtree)

然后在keep-alive组件返回的函数中根据 vnode 对象的 key 去缓存中查找是否有缓存的组件,如果缓存存在,则继承组件实例,并将用于描述组件的 vnode 对象标记为 COMPONENT_KEPT_ALIVE,这样渲染器就不会重新创建新的组件实例;如果缓存不存在,则将 vnode 对象的key 添加到 keys 集合中,然后判断当缓存数量超过指定阈值时就对缓存进行修剪。如下代码所示:

return () => {
  // 如果 vnode 上不存在 key,则使用 vnode.type 作为key
  const key = vnode.key == null ? comp : vnode.key
  // 根据 vnode 的key去缓存中查找是否有缓存的组件
  const cachedVNode = cache.get(key)

  if (vnode.el) {
    vnode = cloneVNode(vnode)
    if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
      rawVNode.ssContent = vnode
    }
  }
  the normalized vnode) in
  pendingCacheKey = key

  if (cachedVNode) {
    // 如果缓存中存在缓存的组件
    vnode.el = cachedVNode.el
    // 继承缓存的组件
    vnode.component = cachedVNode.component
    // 如果组件上有动画,处理动画
    if (vnode.transition) {
      setTransitionHooks(vnode, vnode.transition!)
    }
    vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
    keys.delete(key)
    keys.add(key)
  } else {
    // 将 vnode 的key 添加到 keys 集合中
    keys.add(key)
    // 当缓存数量超过指定阈值时对缓存进行修剪
    if (max && keys.size > parseInt(max as string, 10)) {
      pruneCacheEntry(keys.values().next().value)
    }
  }
}

Vue.js 中采用的修剪策略叫作 “最新一次访问”,其核心在于,把当前访问 (或渲染) 的组件作为最新一次渲染的组件,并且该组件在缓存修剪过程中始终是安全的,即不会被修剪。如下面的代码所示:

function pruneCacheEntry(key: CacheKey) {
  // 从缓存对象中获取缓存的 VNode
  const cached = cache.get(key) as VNode
  // 如果没有当前渲染的组件或当前渲染的组件和缓存中的组件不是同一个,卸载缓存中的组件
  if (!current || cached.type !== current.type) {
    unmount(cached)
  } else if (current) {
    // 当前活动实例不应再保持活动状态。
    resetShapeFlag(current)
  }
  cache.delete(key)
  keys.delete(key)
}

总结

keep-alive 组件的作用类似于 HTTP 中的持久链接。它可以避免组件实例不断地被销毁和重建。keep-alive 的实现并不复杂。当被 keep-alive 的组件在卸载的时候,渲染器并不会真的将其卸载,而是会将该组件搬运到一个隐藏的容器中,从而使得组件可以维持当前状态。当被 keep-alive 的组件再次被 “挂载” 时,渲染器也不会真的挂载它,而是将它从隐藏容器中搬运到原容器。