关于 keep-alive 组件

概述

KeepAlive 组件实现了缓存的功能,将不活动的组件放到内存中,而不是直接销毁他们,并且他不会被渲染成一个真实的元素,主要的使用场景为”在切换页面保存表单内容”。

本文将从源码入手,分析 KeepAlive 的实现方式以及使用上的注意事项,并简单提一下 KeepAlive 不会被渲染成真实的元素的原因。

实现

现版本的 keep-alive.js 总共有 124 行,文件内既有组件,又有外部的功能函数,先从组件入手。

组件整体

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
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
}

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

destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}

mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}

KeepAlive 组件包含了 4 个 props,通过 render 函数进行渲染,并且在 created 钩子中定义了 cache 和 keys,mounted 中设置了两个 watch。

根据文档可以得知,props 中的 include 表示 “需要缓存的组件名称或 RegExp“,exclude 表示的是 “不需要缓存的组件名称或 RegExp“,max 表示 “缓存组件的数目上限“。

cache 是缓存本身,Object.create(null) 会创建一个 “非常干净” 的对象,这个对象不从 Object 处继承任何函数,纯粹是为了储存键值对,键是名称,值是虚拟节点 vnode。

keys 是一个数组,储存的是 vnode 的名称,他和 cache 的键在功能上并不重复,因为他是为了保持 vnode 顺序而存在的。

render 函数

接下来看 render 函数:

1
2
3
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions

slot 是 KeepAlive 组件的默认插槽,他是一个数组,vnode 在插槽数组中找到第一个组件的虚拟节点,关键点是 “第一个“ 和 “组件“ 。

也就是说,如果 KeepAlive 的第一个子元素是 div,那么 div 会被忽略,如果同时有多个组件作为子元素,那么会选用第一个

componentOptions 是虚拟节点 vnode 对应组件的信息或者 undefined。

render 的第一部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
render () {
if (componentOptions) {
// check pattern
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}

// 省略第二部分代码
}
return vnode || (slot && slot[0])
}

接下来是一个判断,如果 vnode 对应的不是组件,那么直接显示 vnode 或者默认插槽的第一项,要注意的是 “第一项“,并不会原样显示。

那么按照 true 的情况,会通过 getComponentName 函数取得组件名称 name,函数实现如下:

1
2
3
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}

Ctor组件的构造函数,这里优先使用组件中定义的 name,也就是开发者自己给组件设置的,备选方案是组件自己生成的 tag,比如 “vue-component-14“。

接着从 KeepAlive 的 props 中取得实际的 include 与 exclude,判断条件如下:

  1. 组件名称 name 不在 include
  2. 组件名称 name 存在于 exclude
  3. include 与 exclude 都未定义

满足任意一项都不用看下去了,这表示组件不在缓存目标里,直接显示该组件的 vnode。

matches 函数是用来匹配的,通过数组、字符串和正则表达式的方式,匹配 name 与 include / exclude,代码没啥展示的意义。

render 的第二部分

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
render () {
if (componentOptions) {

// 省略第一部分代码

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])
}

首先得到虚拟节点 vnode 的唯一 key,根据 key 检查缓存是否命中,如果命中了,那就把 vnode 对应的组件实例显示出来,然后将 key 移动到 key 的末尾。

如果未命中,那么将 vnode 放到缓存容器 cache 中,将 key 放到 keys 中,接着是判断缓存数量是否超过上限 max,超过了就调用 pruneCacheEntry(cache, keys[0], keys, this._vnode)

来看看 pruneCacheEntry 的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
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
remove(keys, key)
}

这个函数的功能是将指定的 vnode 移出缓存容器,在 cached 未被渲染时会先将组件实例销毁。

所以结合上下文, pruneCacheEntry(cache, keys[0], keys, this._vnode) 的作用是在超过缓存上限 max 后,将缓存中的第一个组件删除

render 总结

render 函数会在每一次渲染的时候执行,他会 “监听” 默认插槽第一个 vnode 的变化,得到对应的组件信息,如果组件的名称不在要缓存的名单里,那么就直接渲染

否则构建出组件的唯一 key,看看这个 key 是否命中缓存,是就从缓存里拿出实例去渲染,否则将其缓存,并考虑缓存数量是否过上限。

mounted

mounted 中定义了两个 watch 函数,监听 include 与 exclude 的变化,通过 pruneCache 将不在缓存名单的 vnode 即时移除

destroy

在 KeepAlive 组件被销毁时,遍历缓存容器 cache,将里面的 vnode 全部移除。

抽象组件

KeepAlive 组件对象中设置了一句 abstract: true,这项设置可以让组件不被渲染成真实元素,并且 KeepAlive 的子元素的 $parent 会指向 KeepAlive 的父元素,同样的,Transition 也是一个抽象组件。

总结

其实要注意的地方刚刚已经提过了,KeepAlive 只关注默认插槽中的第一个 Vue 组件,在没有满足条件的组件时会直接显示 vnode 或默认插槽第一项,被缓存的 vnode 也会跟着 include 与 exclude 不断发生变化,并不是缓存后就保持不变等着取用的。