Vue源码阅读笔记——keep-alive 原理

296 阅读4分钟

一起养成写作习惯!这是我参与「掘金日新计划 · 4 月更文挑战」的第9天,点击查看活动详情

一、用法

<keep-alive> 包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。

当组件在 <keep-alive> 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行。

<keep-alive> 是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。

二、属性

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

三、何时用到keep-alive

打个比方,举个例子:我们在后台管理系统中有列表页,头部有查询条件,我们点击一条信息打开详情页,这时我们切换头部的tab又或者是返回希望查询条件还在,此时我们就可以使用keep-alive来做缓存。

四、源码分析

keep-alive核心源码主要在src/core/components/keep-alive.js中。

image.png 上图我们看到:

  1. 定义了namekeep-alive
  2. abstract: true,抽象组件:不会渲染为 DOM 元素,也不会出现在组件的父组件链中。
  3. props接受三个参数,参数的意思在上面说过。
  4. 然后就是几个生命周期函数,render方法几个主要部分。

created

created () {
    this.cache = Object.create(null)
    this.keys = []
},
  • 定义了cache对象,到时候存放需要缓存的DOM节点
  • keys来存放缓存的key

render

render () {
    const slot = this.$slots.default
    console.log('slot', slot)
    const vnode: VNode = getFirstComponentChild(slot)
    console.log('vnode', vnode)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    console.log('componentOptions', componentOptions)
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      console.log('name', name)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode // 不需要缓存的直接return vnode
      }

      const { cache, keys } = this
      // 我们拿key,节点没有的话我们声明一个key
      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]) {
        // 如果有缓存,直接读取缓存中的componentInstance赋值
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        remove(keys, key) // 然后就是刷新key了,先移除,再push
        keys.push(key)
      } else {
        // 没有缓存的话就缓存下做一个赋值操作,供后面缓存节点
        // delay setting the cache until update
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      // 这里其实是个核心,将keepAlive设置为true,然后切换组件命中缓存之后,就会直接取缓存节点
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
}

render里面的东西还是有点的,我们先写个demo然后一边调试一边看:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <button @click="handleClick('listCom')">列表</button>
    <button @click="handleClick('detailCom')">详情</button>
    <keep-alive>
      <component :is="whileComponent"></component>
    </keep-alive>
  </div>

  <script src="../../dist/vue.js"></script>
  <script>

    var listCom = {
      template: `
      <div>
        <h1>列表组件</h1>
        <input />
      </div>
      `
    }

    var detailCom = {
      template: `
      <div>
        <h1>详情组件</h1>
        <input />
      </div>
      `
    }

    var vm = new Vue({
      el: '#app',
      components: {
        listCom, detailCom
      },
      data() {
        return {
          whileComponent: 'listCom'
        }
      },
      methods: {
        handleClick(componentId) {
          this.whileComponent = componentId
        },
      },
    })
  </script>
</body>
</html>

例子比较简单把,点击列表按钮和详情按钮来切换组件,然后我们刷新页面:

  • 首先通过插槽方式拿到keep-alive里面的组件,然后拿到第一个子组件的节点,然后获取该子节点的componentOptions,我们看下这几个打印结果。

image.png

  • 然后走到if里面的逻辑通过getComponentName方法获取他的name
function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

我们这时候是需要缓存的(不缓存的话直接返回vnode),我们根据节点的key来判断没有缓存过,如果有,则读取然后刷新keys;如果没有就添加缓存。

mounted

mounted () {
    this.cacheVNode()
    this.$watch('include', val => { // 监听白名单
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => { // 监听黑名单
      pruneCache(this, name => !matches(val, name))
    })
},
  • 首先他有一个cacheVNode方法
cacheVNode() {
      const { cache, keys, vnodeToCache, keyToCache } = this
      // 其实就是添加缓存
      if (vnodeToCache) {
        const { tag, componentInstance, componentOptions } = vnodeToCache
        cache[keyToCache] = {
          name: getComponentName(componentOptions),
          tag,
          componentInstance,
        }
        keys.push(keyToCache)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
        this.vnodeToCache = null
      }
}

render中,对于需要缓存的节点我们赋值了vnodeToCachekeyToCache,在这里就是把他们添加到缓存cachekeys里面去,然后去判断有没有超过最大缓存数量,超过的时候执行了pruneCacheEntry

function pruneCacheEntry (
  cache: CacheEntryMap,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  // 其实就是超过最大缓存数量的时候,将第一个缓存的节点移除
  const entry: ?CacheEntry = cache[key]
  if (entry && (!current || entry.tag !== current.tag)) {
    entry.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}
  • 然后是监听includeexclude,执行了pruneCache
function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const entry: ?CacheEntry = cache[key]
    if (entry) {
      // 主要就是将监听的黑白名单过滤下,将不需要缓存的移除掉
      const name: ?string = entry.name
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}

updated

  updated () {
    // 组件重新渲染的时候,执行cacheVNode添加缓存
    this.cacheVNode()
  }

destroyed

 destroyed () {
    // 销毁组件的时候,循环去移除缓存
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
}

注意它清除缓存不是简单的将cachekeys制空。

总结

keep-alive的核心源码我们阅读完了,相信大家一定有所收获;其实如果想真正理解它实现的原理,我们还需要配合vue渲染过程来阅读理解,今天我们先到这里~