[vue]keep-alive组件使用及深入

663 阅读2分钟

基本使用

<keep-alive>
    <router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />

如果我们想缓存一个页面或者组件,将它放置在 keep-alive 组件中,就能神奇地实现缓存组件实例。例如:页面的滚动位置、输入框内的内容在切换的过程中都不会丢失。

平时使用就当作是一个黑盒子一样,keep-alive 帮我们完成缓存的需求,今天来看下它的到底是怎么实现这一功能的。

源码分析

自我实现代码思路

在打开源码前试想下如果是自己,该如何去缓存我们页面状态呢。来让我自己先设计下。
页面都是有一个或者多个组件构成的,也就是我们常写的 .vue 文件。

<template>
    <div>{{ msg }}</div>
</template>
<script>
    export default {
        data() {
            msg: 'hello'
        }
    }
</script>

看下它是如何转化渲染成最终的 DOM 的

image.png

现在我们来看下 keep-alive 的页面跳转的表现,例如从 A -> B 页面跳转,然后从 B -> A 返回 A 页面的状态还保存。
从页面 dom 处分析:

  1. A -> B 时候 A 页面相关的 DOM 是卸载了,所以排除通过 css 来隐藏 A 页面 DOM 的情况。所以 B -> A 的时候肯定是经历了 patch DOM 的过程。
  2. 所以我们只能在 vnode 这层做文章,我们需要保存 A 页面的 vnode, 因为这个 vnode 包含了关于页面 A 的状态,当页面再次加载时把之前的状态都重新渲染出来。

源码

我看的是 Vue 2.6.12版本的源码,打开 vue/src/core/components/keep-alive.js, 虽然我们前面梳理了下大概的思路,当我们打开 keep-alive.js 文件惊奇发现代码只有 124行,而且代码看着很清晰。

来看下主流程吧
初始化 cachekeys 变量,用于缓存和标记组件用。

  created () {
    this.cache = Object.create(null)
    this.keys = []
  },

核心逻辑

render(){
   const { cache, keys } = this
   const key: ?string = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
  if (cache[key]) {
    vnode.componentInstance = cache[key].componentInstance
    // make current key freshest
    remove(keys, key)
    keys.push(key)
  } else {
    cache[key] = vnode
    keys.push(key)
    // prune oldest entry
    if (this.max && keys.length > parseInt(this.max)) {
      pruneCacheEntry(cache, keys[0], keys, this._vnode)
    }
  }
  vnode.data.keepAlive = true
  return vnode || (slot && slot[0])
}
  1. 首先我们来看下通过 cache[key] 判断组件是否有缓存,这个 key 是组件的唯一标识,我们暂时认为通过它来拿到组件就行,后序可以看看它是如何获取这个标识的。
  2. 当我们首次访问组件时候,cache[key] 肯定为 undefined 走的下面的逻辑,此时我们将组件的 vnode 保存在 cache 的唯一 key 下。并且将 key push 到 keys 中,keys 是记录组件最近活跃组件的,当我们设置 max 最大缓存组件数时候需要通过它来判断,活跃最久远的组件销毁。
  • pruneCacheEntry 的逻辑暂时不关注,它是额外的功能。我们目前只关心如何缓存组件。

经过上面的两步,已经将组件对应的 vnode 缓存到 cache 对象中,并且更新了活跃组件列表 keys 。此时相当于访问了 A 页面。

如果我们从 B -> A 再次跳转回 A 页面时候,我们就回走上面的逻辑。

  1. 此时并没有直接返回缓存到 vnode, 而是将缓存实例 cache[key].componentInstance 添加到 vnode 对象上。
  • vnode.componentInstance 对象是只有渲染过的组件才存在,第一次进入或者没有缓存的组件的 vnode.componentInstance 是为 undefined
  1. 后面的逻辑还是跟首次访问一样,设置 vnode.data.keepAlive=true 最终返回 vnode

到这里就完成缓存组件再次访问时拿到 vnode 的过程,至于如何去渲染缓存的 vnode,我们继续往下看。

keep-alive patch 流程

有了 vnode 后,需要将它转化为 dom, patch 流程在 core/vdom/patch.js

渲染 keep-alive 组件,对应的流程是

if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
  return
}
  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm)
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }
  1. isReactivated 对应的就是在 keep-alive render 函数中的 vnode.componentInstancevnode.data.keepAlive=true 设置。
  2. 走到 initComponent(vnode, insertedVnodeQueue),拿到 vnode.componentInstance.$el 缓存的 DOM 元素
  3. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm) 将 dom 挂在到 parentElm 上。

到此完成了 keep-alive 组件的渲染。

keep-alive 的其他用法实现

keep-alive 还接受其他 props。

  • include - 字符串或正则表达式。只有名称匹配的组件会被缓存。
  • exclude - 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
  • max - 数字。最多可以缓存多少组件实例。

拿 include 的实现作为举例:

export default {
  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },
  mounted () {
    // 发现 include 发生变化后,执行 pruneCache 移除不符合规则的组件缓存
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
  }
}

来看下 pruneCahe 函数的实现

function pruneCache (keepAliveInstance: any, filter: Function) {
  // keepAliveInstance 就是传递 this,也就是 keep-alive 组件
  const { cache, keys, _vnode } = keepAliveInstance
  // 遍历缓存组件
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      // 获取组件名,也就是我们在组件中定义的 name 选项
      const name: ?string = getComponentName(cachedNode.componentOptions)
      /**
      * 执行 matches 函数判断,组件名是否符合 include 的规则
      * 如果不符合,则执行 pruneCacheEntry 移除缓存组件
      */
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

来看下 pruneCacheEntry 函数实现

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  // 移除缓存组件
  cache[key] = null
  // 移除 keys 对应的活跃组件
  remove(keys, key)
}

总结

keep-alive 组件的流程判断如下,当拿到 keep-alive 组件的 vnode 时然后进行 patch 挂载 DOM 流程。 Screen Shot 2022-05-09 at 3.45.40 PM.png