Vue.js設計與實現18-KeepAlive的原理與實現
1.寫在前面
前面文章介紹了Vue.js通過渲染器實現組件化的能力,介紹了有狀態組件和無狀態組件的構造與實現,還有異步組件對于框架的意義。本文將主要介紹Vue.js的重要內置組件和模塊--KeepAlive組件。
2.KeepAlive組件
KeepAlive字面意思理解就是保持鮮活,就是建立持久連接的意思,可以避免組件或連接頻繁地創建和銷毀。
<template>
<KeepAlive>
<Tab v-if="currentTab === 1"/>
<Tab v-if="currentTab === 2"/>
<Tab v-if="currentTab === 3"/>
</KeepAlive>
</template>
在上面代碼中,會根據currentTab變量的值頻繁切換Tab組件,會導致不停地卸載和重建對應的Tab組件,為了避免因此產生的性能開銷,可以使用KeepAlive組件保持組件的鮮活。那么KeepAlive組件是如何保持組件的鮮活的,其實就會對組件進行緩存管理,避免組件頻繁的卸載和重建。
其實,就是通過一個隱藏的組件緩存容器,將組件需要的時候將其放到容器里,在需要重建使用的時候將其取出,這樣對于用戶感知是進行了“卸載”和“重建”組件。在組件搬運到緩存容器和搬出,就是對應組件的生命周期activated和deactivated。
3.組件的失活和激活
那么,應該如何實現組件的緩存管理呢?
const KeepAlive = {
// keepAlive組件的標識符
_isKeepAlive:true,
setup(props,{slots}){
//緩存容器
const cache = new Map();
const instance = currentInstance;
const { move, createElement } = instance.keepAliveCtx;
//隱藏容器
const storageContainer = createElement("div");
instance._deActivate = (vnode)=>{
move(vnode, storageContainer)
};
instance._activate = (vnode, container, anchor)=>{
move(vnode, container, anchor)
};
return ()=>{
let rawNode = slots.default();
// 非組件的虛擬節點無法被keepAlive
if(typeof rawNode.type !== "object"){
return rawNode;
}
//在掛在時先獲取緩存的組件vnode
const cacheVNode = cache.get(rawNode.type);
if(cacheVNode){
rawVNode.component = cacheVNode.component;
rawVNode.keptAlive = true;
}else{
cache.set(rawVNode.type, rawVNode);
}
rawVNode.shouldKeepAlive = true;
rawVNode.keepAliveInstance = instance;
// 渲染組件vnode
return rawVNode
}
}
}
在上面代碼中,KeepAlive組件本身不會渲染額外的內容,渲染函數只返回被KeepAlive的組件,被稱為“內部組件”,KeepAlive會在“內部組件”的Vnode對象上添加標記屬性,便于渲染器執行特定邏輯。
- shouldKeepAlive屬性會被添加到“內部組件”的vnode對象上,當渲染器卸載“內部組件”時,可以通過檢查屬性得知“內部組件”是否需要被KeepAlive。
- keepAliveInstance:內部組件的vnode對象會持有keepAlive組件實例,在unmount函數中通過keepAliveInstance訪問_deactivate函數。
- keptAlive:內部組件已被緩存則添加keptAlive標記,判斷內部組件重新渲染時是否需要重新掛載還是激活。
function unmount(vnode){
if(vnode.type === Fragment){
vnode.children.forEach(comp=>unmount(comp));
return;
}else if(typeof vnode.type === "object"){
if(vnode.shouldKeepAlive){
vnode.keepAliveInstance._deactivate(vnode);
}else{
unmount(vnode.component.subTree);
}
return
}
const parent = vnode.el.parentVNode;
if(parent){
parent.removeChild(vnode.el);
}
}
組件失活的本質是將組件所渲染的內容移動到隱藏容器中,激活的本質是將組件所要渲染的內容從隱藏容器中搬運回原來的容器。
const { move, createElement } = instance.keepAliveCtx;
instance._deActivate = (vnode)=>{
move(vnode, storageContainer);
}
instance._activate = (vnode, container, anchor)=>{
move(vnode, container, anchor);
}
4.include和exclude
我們看到上面的代碼會對組件所有的"內部組件"進行緩存,但是使用者又想自定義緩存規則,只對特定組件進行緩存,對此KeepAlive組件需要支持兩個props:include和exclude。
- include:用于顯式配置應被緩存的組件
- exclude:用于顯式配置不應該被緩存的組件
const cache = new Map();
const keepAlive = {
__isKeepAlive: true,
props:{
include: RegExp,
exclude: RegExp
},
setup(props, {slots}){
//...
return ()=>{
let rawVNode = slots.default();
if(typeof rawVNode.type !== "object"){
return rawVNode;
}
const name = rawVNode.type.name;
if(name && (
(props.include && !props.include.test(name)) ||
(props.exclude && props.include.test(name))
)){
//直接渲染內部組件,不對其進行緩存操作
return rawVNode
}
}
}
}
上面代碼中,為了簡便闡述問題進行設置正則類型的值,在KeepAlive組件被掛載時,會根據"內部組件"的名稱進行匹配,根據匹配結果判斷是否要對組件進行緩存。
5.緩存管理
在前面小節中使用Map對象實現對組件的緩存,Map的鍵值對分別對應的是組件vnode.type屬性值和描述該組件的vnode對象。因為用于描述組件的vnode對象存在對組件實例的引用,對此緩存用于描述組件的vnode對象,等價于緩存了組件實例。
前面介紹的keepAlive組件實現緩存的處理邏輯是:
- 緩存存在時繼承組件實例,將描述組件的vnode對象標記為keptAlive,渲染器不會重新創建新的組件實例
- 緩存不存在時,則設置緩存
但是,如果緩存不存在時,那么總是會設置新的緩存,這樣導致緩存不斷增加,會占用大量內存。對此,我們需要設置個內存閾值,在緩存數量超過指定閾值時需要對緩存進行修剪,在Vue.js中使用的是"最新一次訪問"策略。
"最新一次訪問"策略本質上就是通過設置當前訪問或渲染的組件作為最新一次渲染的組件,并且該組件在修剪過程中始終是安全的,即不會被修剪。
緩存實例中需要滿足固定的格式:
const _cache = new Map();
const cache: KeepAliveCache = {
get(key){
_cache.get(key);
},
set(key, value){
_cache.set(key, value);
},
delete(key){
_cache.delete(key);
},
forEach(fn){
_cache.forEach(fn);
}
}
6.寫在最后
本文簡單介紹了Vue.js中KeepAlive組件的設計與實現原理,可以實現對組件的緩存,避免組件實例不斷地銷毀和重建。KeepAlive組件卸載時渲染器并不會真實地把它進行卸載,而是將該組件搬運到另外一個隱藏容器里,從而使得組件能夠維持當前狀態。在KeepAlive組件掛載時,渲染器將其從隱藏容器中搬運到原容器中。此外,我們還討論了KeepAlive組件的include和exclude自定義緩存,以及緩存管理。