# keep-alive实现原理

# keep-alive渲染过程分析

我们将keep-alive的整个过程拆分为如下几个阶段:

  1. created阶段,创建对象cache对象,数组keys。其中key为每一个组件的唯一标识;
  2. render阶段,获取第一个子组件:
    1. 如果不在include列表中,或者在exclude列表中,则直接返回vnode,不做缓存;
    2. 如果cache[key]存在,则表示命中了缓存,直接从缓存中获取vnode。同时,移除老的key,并将新的key存放到数组尾部,以保障当前的vnode是最新鲜的(参考LRU缓存策略);
    3. 如果cache[key]不存在,则表示没有缓存。在cache对象上存储对应的vnode,即cache[key] = vnode。同时,将key放到keys数组尾部。如果超过max设置的最大缓存个数,则删除数组中的第一个key,并销毁key对应的vnode的实例。
    4. 最后返回vnode;
  3. mounted阶段:
    1. 监控include、exclude的变化,并调用pruneCache (opens new window)从缓存中删除对应的key,并相应的销毁对应的vnode实例。
  4. destroyed阶段:
    1. 调用pruneCacheEntry (opens new window),销毁所有的vnode组件实例。


以下是keep-alive的源码,我们给每个阶段添加了注释,详细大家根据keep-alive的整个渲染过程,可以详细了解其实现过程。

/* @flow */

import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'

type VNodeCache = { [key: string]: ?VNode };

// 获取组件名称(通过组件选项options中的name值,或者opts.tag)
function getComponentName (opts: ?VNodeComponentOptions): ?string {
  return opts && (opts.Ctor.options.name || opts.tag)
}

// 判断name是否匹配pattern
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  /* istanbul ignore next */
  return false
}

// 移除keepAlive实例中的不满足filter函数的key
function pruneCache (keepAliveInstance: any, filter: Function) {
  const { cache, keys, _vnode } = keepAliveInstance
  for (const key in cache) {
    const cachedNode: ?VNode = cache[key]
    if (cachedNode) {
      const name: ?string = getComponentName(cachedNode.componentOptions)
      if (name && !filter(name)) {
        pruneCacheEntry(cache, key, keys, _vnode)
      }
    }
  }
}
// 移除cache中key对应的vnode,并销毁vnode对应的组件实例
function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  if (cached && (!current || cached.tag !== current.tag)) {
    // 销毁vnode对应的vnode实例
    cached.componentInstance.$destroy()
  }
  // 从cache对象中移除vnode
  cache[key] = null
  // 从keys数组中移除对应的key
  remove(keys, key)
}

const patternTypes: Array<Function> = [String, RegExp, Array]

export default {
  name: 'keep-alive',
  abstract: true,

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

  created() {
    // 创建cache对象,用于存储vnode,格式为: { key: vnode }
    this.cache = Object.create(null)
    // 存储vnode对应的key
    this.keys = []
  },

  destroyed() {
    // 遍历this.cache中的key,并删除对应的缓存entry
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys)
    }
  },

  mounted() {
    // 监控include、exclude的变化,并移除对应的cache对象
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render() {
    // 获取默认插槽, 它是一个vnode数组
    const slot = this.$slots.default
    // 获取第一个vnode
    const vnode: VNode = getFirstComponentChild(slot)
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      // 通过vnode.componentOptions,获取组件名称
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included,在include中,但是名字没有,或名字不匹配
        (include && (!name || !matches(include, name))) ||
        // excluded,组件在excluded当中,并且name存在,并满足exclude的条件
        (exclude && name && matches(exclude, name))
      ) {
        // 直接返回vnode
        return vnode
      }

      const { cache, keys } = this
      // 获取vnode的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]) {
        // 缓存命中
        vnode.componentInstance = cache[key].componentInstance
        // make current key freshest
        // 保持当前key对应的那个vnode是新鲜的(移除老的那个key,并将新的key存到队列尾部): LRU缓存策略
        remove(keys, key)
        keys.push(key)
      } else {
        // 缓存没有命中,则缓存vnode到cache[key]上
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        // 如果超过了max值,则删除最老的那个vnode(keys数组中的第一个)
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }
      // 如果是keep-alive的组件,则在vnode的data上设置keepAlive属性为true
      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0])
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142

# 策略算法

常见的策略算法主要包括以下几种:

  • FIFO(first-in-first-out) 先进先出策略
  • LRU(least-recently-use) 最近最少使用策略
  • LFU(least-frequently-used)计数最少策略

接下来,我们以流程图的方式,大概介绍下上面几种算法的原理。

# FIFO策略

我们通过记录数据使用的时间,当缓存大小即将溢出时,优先清除离当前时间最远的数据。即先入栈的,先出栈。

image.png

# LRU策略

以时间作为参考,如果数据最近被访问过,那么将来被访问的几率会更高。如果以一个数组去记录数据,当有一数据被访问时,该数据会被移动到数组的末尾,表明最近被使用过。当缓存溢出时,会删除数组的头部数据。即,将最久不使用的数据移除(保留最新鲜的数据)。
我们以图的方式来展现,假设我们默认设置的最大内存为5。
image.png
第1-5次:A-E依次入栈。此时队列元素顺序为:
队头 :E -> D -> C -> B -> A  队尾

第6次:访问F,队尾的A出栈,F入栈。此时栈内元素顺序为:
队头 :F -> E -> D -> C -> B -> ~~ A ~~  队尾

第7次:访问C,C在栈内。此时调整C的位置,将C放到队头。此时栈内元素顺序为:
队头 :C -> F -> E -> D -> B  队尾

第8次:访问G,队尾的B出栈,G入栈。此时栈内元素顺序为:
队头 :G -> C -> F -> E -> D-> ~~ B ~~  队尾

# LFU策略

队列从头到尾,按照使用次数,降序排列。

以次数作为参考,用次数去标记数据使用频率,次数最少的会在缓存溢出时被淘汰。

image.png第1次:
队头 A(32) -> B(30) -> C(26) -> D(26) -> E(25) 队尾

第2次:访问D,此时D的计数大于C,D、C互换位置:
队头 A(32) -> B(30) -> D(27) -> C(26) -> E(25) 队尾

第3次:访问B,此时B为31次,位置不变:
队头 A(32) -> B(31) -> D(27) -> C(26) -> E(25) 队尾

第4次:访问F。F是新元素,最小计数的E(25)出栈,同时F(1)入栈,排到队尾:
队头 A(32) -> B(30) -> D(27) -> C(26) -> F(1) -> ~~ E(25) ~~ 队尾

# 参考