这是我参与2022首次更文挑战的第3天,活动详情查看:2022首次更文挑战
前言
keep-alive是Vue的内置组件。
官方文档这样介绍:keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。它是一个抽象组件,自身不会被渲染为DOM元素,也不会出现在组件的父组件链中。
keep-alive主要是用来保留组件状态或避免重新渲染。
接下来就来具体看下keep-alive是怎么实现的:
基本用法
下边是官方提供的常用的几种使用方法:
keep-alive还提供了三个props:
- include:名称匹配的组件被缓存,可以用逗号分割的字符串,正则表达式或数组。
- exclude:名称匹配的组件不会被缓存,可以用逗号分割的字符串,正则表达式或数组。
- max:最多缓存多少组实例。一旦超过这个数字,在新实例创建之前,已缓存组件中最久没有被访问的实例会被销毁。
源码分析
定义在src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true,
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))
})
},
render () {
...
}
}
整体来看keep-alive就是一个Vue组件,该组件没有写template,而是自定义了render函数。
abstract
看到keep-alive组件首先设置了abstract属性为true,在Vue文档中并没有看到对abstract这个属性的介绍,那这个属性是用来干什么的呢??
其实是为了在建立组件的父子关系时忽略keep-alive组件。具体代码定义在src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
...
}
Vue官方文档中也写到keep-alive是一个抽象组件:不会渲染为一个DOM元素,也不会出现在组件的父组件链中。
created
created () {
this.cache = Object.create(null)
this.keys = []
}
在keep-alive组件的created钩子定义了两个数据属性来实现组件缓存:
cache: 保存已经创建过的组件vnode
keys: 保存所有创建的vnode的key
destroyed
destroyed钩子函数内部,for...in遍历cache对象,依次执行pruneCacheEntry函数去移除缓存中的vnode
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
具体来看下pruneCacheEntry的实现:
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
remove(keys, key)
}
-
首先通过key找到缓存数组中对应的组件vnode
-
对组件vnode的实例执行destroy方法去销毁该实例(destroy方法后边具体看)
-
将缓存对象中当前key对应的vnode值置为null
-
执行remove方法将key从keys数组中移除
remove
在移除key的时候调用了个remove方法,来看下这个方法的实现,定义在src/shared/util.js
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
工具函数很简单,就是使用splice方法移除数组中的对应项。
mounted
mounted函数主要是监听了include和exclude属性。当发生变化时,执行prunCache函数。
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
看一下prunCache函数:
prunCache
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对象,获取对应组件vnode的name值
-
调用matches方法,看是否满足过滤条件
-
如果不满足过滤条件,则直接调用pruneCacheEntry方法删除对应项。这次调用pruneCacheEntry函数时与destroy钩子函数调用有个不一样的点是传了第四个参数(当前keep-alive组件)。
接下来看一下用来过滤的matches方法:
matches
我们知道include和exclude属性接收三种类型:数组,逗号分割的字符串,正则表达式。matches方法就是针对这三种类型来做判断的。
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
}
-
首先判断匹配类型是不是数组,如果是则判断组件name是否存在数组中
-
接下来判断是不是字符串,如果是则调用split方法将其转化为数组,再判断name是否在数组中
-
最后判断是否是正则,如果是直接调用test判断name是否符合正则条件
render
最后就是缓存组件的重点:render函数了
render () {
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
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
}
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])
}
-
首先获取keep-alive的默认插槽,然后调用getFirstComponentChild获取插槽的第一个子节点(keep-alive只处理第一个子元素)
-
获取组件参数vnode.componentOptions赋值给componentOptions变量
-
如果componentOptions不存在,则直接返回该节点(slot部分的实现后边单独来看)
-
componentOptions存在,则首先获取传入keep-alive的子组件的名称,如果没设置名称,则直接拿tag
-
接下来根据include和exclude属性来判断传入的组件是否满足条件,如果不满足,则直接返回该vnode
-
如果满足条件,则获取被包裹的组件的key(会先判断vnode.key值是否为空,如果为空则用组件的cid+tag来代替key)
-
在缓存对象中查找该key对应的组件vnode。如果存在,则获取对应的实例,并且从keys中将该key移除,然后再重新添加到最后边。(之所以要先删除再添加主要是为了提高该key的存活,LRU缓存策略)
-
如果该key在缓存对象中不存在,则添加对应的vnode到缓存对象,同时添加key到keys数组中。最后判断keys的长度是不是超过了max限制,如果超过了,则删除缓存中的第一个(这也是为什么每次缓存命中都要先删除key再添加的原因)。