keep-alive 组件是什么?
<keep-alive>
是 Vue
的内置组件,用来包裹组件,达到缓存组件实例的作用~
Vue
官网文档中提到:
<keep-alive>
是一个形象组件:它本身不会渲染一个DOM
元素,也不会呈现在组件的父组件链中。- 当组件在
<keep-alive>
内被切换,它的activated
和deactivated
这两个生命周期钩子函数将会被对应执行。
keep-alive 组件原理
匹配和过滤
include
– 字符串或正则表达式。只有名称匹配的组件会被缓存。exclude
– 字符串或正则表达式。任何名称匹配的组件都不会被缓存。
初始化
created
阶段会初始化两个变量,别离为cache
和keys
。mounted
阶段会对include
和exclude
变量的值做监测。
created () {this.cache = Object.create(null)
this.keys = []},
mounted () {
this.$watch('include', val => {pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {pruneCache(this, name => !matches(val, name))
})
},
能够看到,如果 include
或者 exclude
的值发生变化,就会触发 pruneCache
函数,不过筛选的条件须要依据 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
}
能够看到,pattern
能够有三种取值,别离为:
string
RegExp
Array<string>
之后判断 name
是不是在 pattern
指定的值 / 范畴内,最初再依据后果返回 true
或者 false
。
缓存剔除
无论是 include
还是 exclude
,他们的值都不是变化无穷的,每当过滤条件扭转,都须要从已有的缓存中「剔除」不符合条件的 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)
}
}
}
}
能够看到这里又调用了 pruneCacheEntry
来「剔除」不合乎缓存条件的 key
????
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 和 current 不是同一个节点
// 就把调用 $destroy 把 cached 销毁
if (cached && (!current || cached.tag !== current.tag)) {cached.componentInstance.$destroy()
}
渲染时剔除
下面咱们探讨的是过滤条件的变动状况,如果没变动呢?
其实 <keep-alive>
组件也会在每次渲染的时候都过滤一次组件,保障缓存的组件都曾经通过过滤条件????
render () {
...
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {return vnode}
...
}
缓存淘汰策略
除了下面说到的两个参数,<keep-alive>
组件还能传一个参数:max
文档中提到一个实例销毁规定:缓存最近拜访的实例,销毁缓存中最久没有被拜访的实例。其实这是一种缓存淘汰策略 —— LRU 算法。
更加具体的规定:
- 在缓存下限达到之前,不思考销毁实例
- 对于最新拜访的数据,淘汰的优先级是最低的
- 对于最不常拜访的数据,淘汰的优先级是最高的
这意味着咱们须要两种数据结构来表白这个算法,其中一种数据结构来存储缓存,另外一种用来存储并示意缓存拜访的新旧水平。
Vue 2.x 与 Vue 3.x 中实现的异同
Vue 2.x 用 cache
来存储缓存,用 keys
来存储缓存中拜访的新旧水平,其中 cache
为对象,keys
为数组。
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)
}
}
然而,Vue 3.0 里中采纳的仍然是 LRU 算法,不过用到的 cache
和 keys
的作用跟 2.0 没有区别,然而数据结构却变了,其中:
cache
是用Map
创立的keys
应用Set
创立的
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {vnode = cloneVNode(vnode)
}
cache.set(key, vnode)
if (cachedVNode) {
// copy over mounted state
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
keys.delete(key)
keys.add(key)
} else {keys.add(key)
// prune oldest entry
if (max && keys.size > parseInt(max as string, 10)) {pruneCacheEntry(Array.from(keys)[0])
}
}
显然,在工夫复杂度上 Vue 3.x 要更优,因为 Set
删除某个 key
只须要 $O(1)$ 的工夫复杂度,而数组删除某个 key
却要消耗 $O(n)$ 工夫复杂度。
不过,即便是 Vue 3.x 的写法也还是有点问题的,比方这一段????
pruneCacheEntry(Array.from(keys)[0])
会先将 keys
转成数组再取第一项,复杂度一下子就高了,这里可能是思考到代码量和可读性上的问题,对性能做了取舍,毕竟缓存大量组件的状况也是比拟少见~
扩大 —— 常见的 LRU 算法实现
LRU 算法在实现逻辑的同时,须要满足以下两点:
- 删除、增加缓存的工夫复杂度为 $O(1)$
- 查找缓存的工夫复杂度为 $O(1)$
其中双向循环链表能够满足条件 1,哈希表能够满足条件 2。基于此咱们能够用哈希表 + 双向循环链表相结合的写法来实现 LRU 算法。以下是实现的代码,基于 LeetCode 上的一道题目:LRU 缓存机制,感兴趣的同学无妨看完之后在下面写写看。
哈希表 + 双向循环链表 实现 LRU 算法
/**
* @param {number} capacity
*/
const LRUCache = function (capacity) {this.map = new Map()
this.doubleLinkList = new DoubleLinkList()
this.maxCapacity = capacity
this.currCapacity = 0
};
/**
* @param {number} val
* @param {string} key
*/
const Node = function (val, key) {
this.val = val
this.key = key
this.next = null
this.prev = null
}
const DoubleLinkList = function () {
this.head = null
this.tail = null
}
/**
* @param {Node} node
* 增加节点
*/
DoubleLinkList.prototype.addNode = function (node) {if (!this.head) {
this.head = node
this.tail = node
this.head.next = this.tail
this.head.prev = this.tail
this.tail.next = this.head
this.tail.prev = this.head
} else {
const next = this.head
this.head = node
this.head.next = next
this.head.prev = this.tail
this.tail.next = this.head
next.prev = this.head
}
}
/**
* @param {Node} node
* @return {void}
* 删除双向链表中的节点
*/
DoubleLinkList.prototype.deleteNode = function (node) {
const prev = node.prev
const next = node.next
prev.next = next
next.prev = prev
// 对头尾节点做非凡判断
if (this.head === node) {this.head = next}
if (this.tail === node) {this.tail = prev}
}
/**
* @param {number} key
* @return {number}
*/
LRUCache.prototype.get = function(key) {const { map, doubleLinkList} = this
// 拜访一个 key 的同时,先将它删除,而后再将它置于双向链表的顶部
if (map.has(key)) {const node = map.get(key)
doubleLinkList.deleteNode(node)
doubleLinkList.addNode(node)
return node.val
}
return -1
};
/**
* @param {number} key
* @param {number} value
* @return {void}
*/
LRUCache.prototype.put = function(key, value) {const { map, doubleLinkList} = this
// 如果曾经存在这个 key 了
// 那么只有改 key 对应的值
// 而后将 key 对应的节点放到双向链表的头部即可
if (map.has(key)) {
// 先在 map 中找到他
const node = map.get(key)
// 而后将它删掉
doubleLinkList.deleteNode(node)
// 改掉值
node.val = value
// 之后退出到链表头部
doubleLinkList.addNode(node)
map.set(key, node)
} else {const node = new Node(value, key)
// 如果之前没有这个 key 且缓存已满
if (this.currCapacity === this.maxCapacity) {
// 先将尾巴节点删除
map.delete(doubleLinkList.tail.key)
doubleLinkList.deleteNode(doubleLinkList.tail)
// 而后将新的 key 退出链表和 map
doubleLinkList.addNode(node)
map.set(key, node)
} else {
// 反之缓存没有满
// 间接新的 key 退出链表和 map 即可
doubleLinkList.addNode(node)
map.set(key, node)
// 缓存容量自增
this.currCapacity++
}
}
};
参考资料
- 文档:keep-alive
- Vue 2.x 源码
- Vue 3.x 源码