LRU 緩存-keep-alive 實(shí)現(xiàn)原理
本文首發(fā)于政采云前端團(tuán)隊(duì)博客:LRU 緩存-keep-alive 實(shí)現(xiàn)原理
https://www.zoo.team/article/lru-keep-alive

前言
相信大部分同學(xué)在日常需求開發(fā)中或多或少的會(huì)有需要一個(gè)組件狀態(tài)被持久化、不被重新渲染的場(chǎng)景,熟悉 vue 的同學(xué)一定會(huì)想到 keep-alive 這個(gè)內(nèi)置組件。
那么什么是 keep-alive 呢?
keep-alive 是 Vue.js 的一個(gè) 內(nèi)置組件。它能夠?qū)⒉换顒?dòng)的組件實(shí)例保存在內(nèi)存中,而不是直接將其銷毀,它是一個(gè)抽象組件,不會(huì)被渲染到真實(shí) DOM 中,也不會(huì)出現(xiàn)在父組件鏈中。簡(jiǎn)單的說,keep-alive用于保存組件的渲染狀態(tài),避免組件反復(fù)創(chuàng)建和渲染,有效提升系統(tǒng)性能。keep-alive ?的 ?max 屬性,用于限制可以緩存多少組件實(shí)例,一旦這個(gè)數(shù)字達(dá)到了上限,在新實(shí)例被創(chuàng)建之前,已緩存組件中最久沒有被訪問的實(shí)例會(huì)被銷毀掉,而這里所運(yùn)用到的緩存機(jī)制就是 LRU 算法。
LRU 緩存淘汰算法
LRU( least recently used)根據(jù)數(shù)據(jù)的歷史記錄來淘汰數(shù)據(jù),重點(diǎn)在于保護(hù)最近被訪問/使用過的數(shù)據(jù),淘汰現(xiàn)階段最久未被訪問的數(shù)據(jù)
LRU的主體思想在于:如果數(shù)據(jù)最近被訪問過,那么將來被訪問的幾率也更高

新數(shù)據(jù)插入到鏈表尾部; 每當(dāng)緩存命中(即緩存數(shù)據(jù)被訪問),則將數(shù)據(jù)移到鏈表尾部 當(dāng)鏈表滿的時(shí)候,將鏈表頭部的數(shù)據(jù)丟棄。
實(shí)現(xiàn)LRU的數(shù)據(jù)結(jié)構(gòu)
經(jīng)典的 LRU 一般都使用 ?hashMap ?+ 雙向鏈表。考慮可能需要頻繁刪除一個(gè)元素,并將這個(gè)元素的前一個(gè)節(jié)點(diǎn)指向下一個(gè)節(jié)點(diǎn),所以使用雙鏈接最合適。并且它是按照結(jié)點(diǎn)最近被使用的時(shí)間順序來存儲(chǔ)的。如果一個(gè)結(jié)點(diǎn)被訪問了, 我們有理由相信它在接下來的一段時(shí)間被訪問的概率要大于其它結(jié)點(diǎn)。

不過既然已經(jīng)在 js 里都已經(jīng)使用?Map?了,為何不直接取用現(xiàn)成的迭代器獲取下一個(gè)結(jié)點(diǎn)的 key 值(keys().next())
//?./LRU.ts
export?class?LRUCache?{
??capacity:?number;?//?容量
??cache:?Map<number,?number?|?null>;?//?緩存
??constructor(capacity:?number)?{
????this.capacity?=?capacity;
????this.cache?=?new?Map();
??}
??get(key:?number):?number?{
????if?(this.cache.has(key))?{
??????let?temp?=?this.cache.get(key)?as?number;
??????//訪問到的?key?若在緩存中,將其提前
??????this.cache.delete(key);
??????this.cache.set(key,?temp);
??????return?temp;
????}
????return?-1;
??}
??put(key:?number,?value:?number):?void?{
????if?(this.cache.has(key))?{
??????this.cache.delete(key);
??????//存在則刪除,if?結(jié)束再提前
????}?else?if?(this.cache.size?>=?this.capacity)?{
??????//?超過緩存長(zhǎng)度,淘汰最近沒使用的
??????this.cache.delete(this.cache.keys().next().value);
??????console.log(`refresh:?key:${key}?,?value:${value}`)
????}
????this.cache.set(key,?value);
??}
??toString(){
????console.log('capacity',this.capacity)
????console.table(this.cache)
??}
}
//?./index.ts
import?{LRUCache}?from?'./lru'
const?list?=?new?LRUCache(4)
list.put(2,2)???//?入?2,剩余容量3
list.put(3,3)???//?入?3,剩余容量2
list.put(4,4)???//?入?4,剩余容量1
list.put(5,5)???//?入?5,已滿????從頭至尾?????????2-3-4-5
list.put(4,4)???//?入4,已存在?——>?置隊(duì)尾?????????2-3-5-4
list.put(1,1)???//?入1,不存在?——>?刪除隊(duì)首?插入1??3-5-4-1
list.get(3)?????//?獲取3,刷新3——>?置隊(duì)尾?????????5-4-1-3
list.toString()
//?./index.ts
import?{LRUCache}?from?'./lru'
const?list?=?new?LRUCache(4)
list.put(2,2)???//?入?2,剩余容量3
list.put(3,3)???//?入?3,剩余容量2
list.put(4,4)???//?入?4,剩余容量1
list.put(5,5)???//?入?5,已滿????從頭至尾??????2-3-4-5
list.put(4,4)???//?入4,已存在?——>?置隊(duì)尾????? 2-3-5-4
list.put(1,1)???//?入1,不存在?——>?刪除隊(duì)首?插入1??3-5-4-1
list.get(3)?????//?獲取3,刷新3——>?置隊(duì)尾??????5-4-1-3
list.toString()
結(jié)果如下:
vue 中 Keep-Alive
原理
使用 ?LRU 緩存機(jī)制進(jìn)行緩存,max 限制緩存表的最大容量 根據(jù)設(shè)定的 include/exclude(如果有)進(jìn)行條件匹配,決定是否緩存。不匹配,直接返回組件實(shí)例 根據(jù)組件 ID 和 tag 生成緩存 ?Key ,并在緩存對(duì)象中查找是否已緩存過該組件實(shí)例。如果存在,直接取出緩存值并更新該 key 在 this.keys 中的位置(更新 key 的位置是實(shí)現(xiàn) LRU 置換策略的關(guān)鍵) 獲取節(jié)點(diǎn)名稱,或者根據(jù)節(jié)點(diǎn) cid 等信息拼出當(dāng)前組件名稱 獲取 keep-alive 包裹著的第一個(gè)子組件對(duì)象及其組件名
源碼分析
初始化 keepAlive 組件
const?KeepAliveImpl:?ComponentOptions?=?{
??name:?`KeepAlive`,
??props:?{
????include:?[String,?RegExp,?Array],
????exclude:?[String,?RegExp,?Array],
????max:?[String,?Number],
??},
??setup(props:?KeepAliveProps,?{?slots?}:?SetupContext)?{
????//?初始化數(shù)據(jù)
????const?cache:?Cache?=?new?Map();
????const?keys:?Keys?=?new?Set();
????let?current:?VNode?|?null?=?null;
????//?當(dāng)?props?上的?include?或者?exclude?變化時(shí)移除緩存
????watch(
??????()?=>?[props.include,?props.exclude],
??????([include,?exclude])?=>?{
??????include?&&?pruneCache((name)?=>?matches(include,?name));
??????exclude?&&?pruneCache((name)?=>?!matches(exclude,?name));
??????},
??????{?flush:?"post",?deep:?true?}
????);
????//?緩存組件的子樹?subTree
????let?pendingCacheKey:?CacheKey?|?null?=?null;
????const?cacheSubtree?=?()?=>?{
??????//?fix?#1621,?the?pendingCacheKey?could?be?0
??????if?(pendingCacheKey?!=?null)?{
????????cache.set(pendingCacheKey,?getInnerChild(instance.subTree));
??????}
????};
????// KeepAlive 組件的設(shè)計(jì),本質(zhì)上就是空間換時(shí)間。
????//?在?KeepAlive?組件內(nèi)部,
????//?當(dāng)組件渲染掛載和更新前都會(huì)緩存組件的渲染子樹?subTree
????onMounted(cacheSubtree);
????onUpdated(cacheSubtree);
????onBeforeUnmount(()?=>?{
????//?卸載緩存表里的所有組件和其中的子樹...
????}
????return?()=>{
??????//?返回?keepAlive?實(shí)例
????}
??}
}
return?()=>{
??//?省略部分代碼,以下是緩存邏輯
??pendingCacheKey?=?null
??const?children?=?slots.default()
??let?vnode?=?children[0]
??const?comp?=?vnode.type?as?Component
??const?name?=?getName(comp)
??const?{?include,?exclude,?max?}?=?props
??//?key?值是?KeepAlive?子節(jié)點(diǎn)創(chuàng)建時(shí)添加的,作為緩存節(jié)點(diǎn)的唯一標(biāo)識(shí)
??const?key?=?vnode.key?==?null???comp?:?vnode.key
??//?通過?key?值獲取緩存節(jié)點(diǎn)
??const?cachedVNode?=?cache.get(key)
??if?(cachedVNode)?{
????//?緩存存在,則使用緩存裝載數(shù)據(jù)
????vnode.el?=?cachedVNode.el
????vnode.component?=?cachedVNode.component
????if?(vnode.transition)?{
??????//?遞歸更新子樹上的?transition?hooks
??????setTransitionHooks(vnode,?vnode.transition!)
????}
??????//?阻止?vNode?節(jié)點(diǎn)作為新節(jié)點(diǎn)被掛載
??????vnode.shapeFlag?|=?ShapeFlags.COMPONENT_KEPT_ALIVE
??????//?刷新key的優(yōu)先級(jí)
??????keys.delete(key)
??????keys.add(key)
??}?else?{
??????keys.add(key)
??????//?屬性配置?max?值,刪除最久不用的?key?,這很符合?LRU?的思想
??????if?(max?&&?keys.size?>?parseInt(max?as?string,?10))?{
????????pruneCacheEntry(keys.values().next().value)
??????}
????}
????//?避免?vNode?被卸載
????vnode.shapeFlag?|=?ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
????current?=?vnode
????return?vnode;
}
將組件移出緩存表
//?遍歷緩存表
function?pruneCache(filter?:?(name:?string)?=>?boolean)?{
??cache.forEach((vnode,?key)?=>?{
????const?name?=?getComponentName(vnode.type?as?ConcreteComponent);
????if?(name?&&?(!filter?||?!filter(name)))?{
??????//?!filter(name)?即?name?在?includes?或不在?excludes?中
??????pruneCacheEntry(key);
????}
??});
}
//?依據(jù)?key?值從緩存表中移除對(duì)應(yīng)組件
function?pruneCacheEntry(key:?CacheKey)?{
??const?cached?=?cache.get(key)?as?VNode;
??if?(!current?||?cached.type?!==?current.type)?{
????/*?當(dāng)前沒有處在?activated?狀態(tài)的組件
?????*?或者當(dāng)前處在?activated?組件不是要?jiǎng)h除的?key?時(shí)
?????*?卸載這個(gè)組件
????*/
????unmount(cached);?//?unmount方法里同樣包含了?resetShapeFlag
??}?else?if?(current)?{
????//?當(dāng)前組件在未來應(yīng)該不再被?keepAlive?緩存
????//?雖然仍在?keepAlive?的容量中但是需要刷新當(dāng)前組件的優(yōu)先級(jí)
????resetShapeFlag(current);
????//?resetShapeFlag?
??}
??cache.delete(key);
??keys.delete(key);
}
function?resetShapeFlag(vnode:?VNode)?{
??let?shapeFlag?=?vnode.shapeFlag;?//?shapeFlag?是?VNode?的標(biāo)識(shí)
???//?...?清除組件的?shapeFlag
}
keep-alive案例
本部分將使用 vue 3.x 的新特性來模擬 ?keep-alive ?的具體應(yīng)用場(chǎng)景
在 index.vue 里我們引入了 CountUp 、timer 和 ColorRandom 三個(gè)帶有狀態(tài)的組件
在容量為 2 的 中包裹了一個(gè)動(dòng)態(tài)組件
//?index.vue
久久抽插视频
|
青青操网|
国产乱人伦久久免费
|
亚洲sv视频
|
夜色五月丁香久久
|
