.Keep-alive的概念
keep-alive是Vue的一个内置实例,用于缓存组件。当keep-alive当它包裹动态组件时,会缓存不活动的组件实例,而不是销毁。
Keep-alive的用法
当组件在 keep-alive 内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行
Keep-alive的参数 (Props)
include- 字符串或正则达式只有 名称匹配的组件会被缓存exclude- 字符串或正则表达式 任何名称匹配的组件都不会被缓存max- 数字 。 最多可以缓存多少组件实例
原理
keep-alive 的本质是缓存管理和特殊的挂载/卸载逻辑。keep-alive 组件的实现需要渲染器层面的支持。这是因为被 keep-alive 的组件在卸载的时候,渲染器并不会真的将其卸载,而是会将该组件搬运到一个隐藏的容器中,实现 “假卸载”,从而使得组件可以维持当前状态。当被 keep-alive 的组件再次被 “挂载” 时,渲染器也不会真的挂载它,而是将它从隐藏容器中搬运到原容器。
通过 keep-alive 组件插槽,获取第一个子节点。根据 include、exclude 判断是否需要缓存,通过组件的 key,判断是否命中缓存。利用 LRU 算法,更新缓存以及对应的 keys 数组。根据 max 控制缓存的最大组件数量。
-
由于组件会先比被它包裹的组件先执行,所以在执行keep-alive组件的render函数时,会把该组件Vnode缓存到自己定义的cache对象中,并将vnode.data.keepAlive设置为true
-
所以初次渲染的时候isReactivated为false,会跟普通组件一样走正常的init过程。
// isRegExp函数判断是不是正则表达式,remove移除数组中的某一个成员
# // getFirstComponentChild获取VNode数组的第一个有效组件
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type VNodeCache = { [key: string]: ?VNode }; // 缓存组件VNode的缓存类型
// 通过组件的name或组件tag来获取组件名(上面注意的第二点)
function getComponentName(opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 判断include或exclude跟组件的name是否匹配成功
function matches(pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1 // include或exclude是数组的情况
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1 // include或exclude是字符串的情况
} else if (isRegExp(pattern)) {
return pattern.test(name) // include或exclude是正则表达式的情况
}
return false // 都没匹配上(上面注意的二三点)
}
// 销毁缓存
function pruneCache(keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance // keep-alive组件实例
for (const key in cache) {
const cachedNode: ?VNode = cache[key] // 已经被缓存的组件
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
// 若name存在且不能跟include或exclude匹配上就销毁这个已经缓存的组件
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
// 销毁缓存的入口
function pruneCacheEntry(
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key] // 被缓存过的组件
// “已经被缓存的组件是否继续被缓存” 有变动时
// 若组件被缓存命中过且当前组件不存在或缓存命中组件的tag和当前组件的tag不相等
if (cached && (!current || cached.tag !== current.tag)) {
// 说明现在这个组件不需要被继续缓存,销毁这个组件实例
cached.componentInstance.$destroy()
}
cache[key] = null // 把缓存中这个组件置为null
remove(keys, key) // 把这个组件的key移除出keys数组
}
// 示例类型
const patternTypes: Array<Function> = [String, RegExp, Array]
// 向外暴露keep-alive组件的一些选项
export default {
name: 'keep-alive', // 组件名
abstract: true, // keep-alive是抽象组件
// 用keep-alive组件时传入的三个props
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 存储需要缓存的组件
this.keys = [] // 存储每个需要缓存的组件的key,即对应this.cache对象中的键值
},
// 销毁keep-alive组件的时候,对缓存中的每个组件执行销毁
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
// keep-alive组件挂载时监听include和exclude的变化,条件满足时就销毁已缓存的组件
mounted() {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// 重点来了
render() {
const slot = this.$slots.default // keep-alive组件的默认插槽
const vnode: VNode = getFirstComponentChild(slot) // 获取默认插槽的第一个有效组件
// 如果vnode存在就取vnode的选项
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
//获取第一个有效组件的name
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this // props传递来的include和exclude
if (
// 若include存在且name不存在或name未匹配上
(include && (!name || !matches(include, name))) ||
// 若exclude存在且name存在或name匹配上
(exclude && name && matches(exclude, name))
) {
return vnode // 说明不用缓存,直接返回这个组件进行渲染
}
// 匹配上就需要进行缓存操作
const { cache, keys } = this // keep-alive组件的缓存组件和缓存组件对应的key
// 获取第一个有效组件的key
const key: ?string = vnode.key == null
// 同一个构造函数可以注册为不同的本地组件
// 所以仅靠cid是不够的,进行拼接一下
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 如果这个组件命中缓存
if (cache[key]) {
// 这个组件的实例用缓存中的组件实例替换
vnode.componentInstance = cache[key].componentInstance
// 更新当前key在keys中的位置
remove(keys, key) // 把当前key从keys中移除
keys.push(key) // 再放到keys的末尾
} else {
// 如果没有命中缓存,就把这个组件加入缓存中
cache[key] = vnode
keys.push(key) // 把这个组件的key放到keys的末尾
// 如果缓存中的组件个数超过传入的max,销毁缓存中的LRU组件
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 设置这个组件的keepAlive属性为true
}
// 若第一个有效的组件存在,但其componentOptions不存在,就返回这个组件进行渲染
// 或若也不存在有效的第一个组件,但keep-alive组件的默认插槽存在,就返回默认插槽的第一个组件进行渲染
return vnode || (slot && slot[0])
}
}
activated与deactivated
created会创建一个cache对象,用来作为缓存容器,保存vnode节点。
destroyed则在组件被销毁的时候清除cache缓存中的所有组件实例
this.cache = Object.create(null)
this.keys = []
destroyed() {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
render
- 先获取到插槽里的内容
- 调用getFirstComponentChild方法获取第一个子组件,获取到该组件的name,如果有name属性就用name,没有就用tag名。
/* 获取该组件节点的名称 */
const name = getComponentName(componentOptions)
/* 优先获取组件的name字段,如果name不存在则获取组件的tag */
function getComponentName(opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
总结
keep-alive 它可以避免组件实例不断地被销毁和重建。keep-alive 的实现并不复杂。当被 keep-alive 的组件在卸载的时候,渲染器并不会真的将其卸载,而是会将该组件搬运到一个隐藏的容器中,从而使得组件可以维持当前状态。