Vue源碼解析,keep-alive是如何實現(xiàn)緩存的?
前言
在性能優(yōu)化上,最常見的手段就是緩存。對需要經(jīng)常訪問的資源進行緩存,減少請求或者是初始化的過程,從而降低時間或內(nèi)存的消耗。Vue 為我們提供了緩存組件 keep-alive,它可用于路由級別或組件級別的緩存。
但其中的緩存原理你是否了解,組件緩存渲染又是如何工作。那么本文就來解析 keep-alive 的原理。
LRU策略
在使用 keep-alive 時,可以添加 prop 屬性 include、exclude、max 允許組件有條件的緩存。既然有限制條件,舊的組件需要刪除緩存,新的組件就需要加入到最新緩存,那么要如何制定對應(yīng)的策略?
LRU(Least recently used,最近最少使用)策略根據(jù)數(shù)據(jù)的歷史訪問記錄來進行淘汰數(shù)據(jù)。LRU 策略的設(shè)計原則是,如果一個數(shù)據(jù)在最近一段時間沒有被訪問到,那么在將來它被訪問的可能性也很小。也就是說,當限定的空間已存滿數(shù)據(jù)時,應(yīng)當把最久沒有被訪問到的數(shù)據(jù)淘汰。

現(xiàn)在緩存最大只允許存3個組件,ABC三個組件依次進入緩存,沒有任何問題 當D組件被訪問時,內(nèi)存空間不足,A是最早進入也是最舊的組件,所以A組件從緩存中刪除,D組件加入到最新的位置 當B組件被再次訪問時,由于B還在緩存中,B移動到最新的位置,其他組件相應(yīng)的往后一位 當E組件被訪問時,內(nèi)存空間不足,C變成最久未使用的組件,C組件從緩存中刪除,E組件加入到最新的位置
keep-alive 緩存機制便是根據(jù)LRU策略來設(shè)置緩存組件新鮮度,將很久未訪問的組件從緩存中刪除。了解完緩存機制,接下來進入源碼,看看keep-alive組件是如何實現(xiàn)的。
組件實現(xiàn)原理
//?源碼位置: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?()?{
????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])
??}
}
kepp-alive 實際是一個抽象組件,只對包裹的子組件做處理,并不會和子組件建立父子關(guān)系,也不會作為節(jié)點渲染到頁面上。在組件開頭就設(shè)置 abstract 為 true,代表該組件是一個抽象組件。
//?源碼位置: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
??//?...
}
那么抽象組件是如何忽略這層關(guān)系的呢?在初始化階段會調(diào)用 initLifecycle,里面判斷父級是否為抽象組件,如果是抽象組件,就選取抽象組件的上一級作為父級,忽略與抽象組件和子組件之間的層級關(guān)系。
回到 keep-alive 組件,組件是沒有編寫template 模板,而是由 render 函數(shù)決定渲染結(jié)果。
const?slot?=?this.$slots.default
const?vnode:?VNode?=?getFirstComponentChild(slot)
如果 keep-alive 存在多個子元素,keep-alive 要求同時只有一個子元素被渲染。所以在開頭會獲取插槽內(nèi)的子元素,調(diào)用 getFirstComponentChild 獲取到第一個子元素的 VNode。
//?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
}
function?matches?(pattern:?string?|?RegExp?|?Array,?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)
??}
??return?false
}
接著判斷當前組件是否符合緩存條件,組件名與include不匹配或與exclude匹配都會直接退出并返回 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
匹配條件通過會進入緩存機制的邏輯,如果命中緩存,從 cache 中獲取緩存的實例設(shè)置到當前的組件上,并調(diào)整 key 的位置將其放到最后。如果沒命中緩存,將當前 VNode 緩存起來,并加入當前組件的 key。如果緩存組件的數(shù)量超出 max 的值,即緩存空間不足,則調(diào)用 pruneCacheEntry 將最舊的組件從緩存中刪除,即 keys[0] 的組件。之后將組件的 keepAlive 標記為 true,表示它是被緩存的組件。
function?pruneCacheEntry?(
??cache:?VNodeCache,
??key:?string,
??keys:?Array,
??current?:?VNode
)?{
??const?cached?=?cache[key]
??if?(cached?&&?(!current?||?cached.tag?!==?current.tag))?{
????cached.componentInstance.$destroy()
??}
??cache[key]?=?null
??remove(keys,?key)
}
pruneCacheEntry 負責將組件從緩存中刪除,它會調(diào)用組件 $destroy 方法銷毀組件實例,緩存組件置空,并移除對應(yīng)的 key。
mounted?()?{
??this.$watch('include',?val?=>?{
????pruneCache(this,?name?=>?matches(val,?name))
??})
??this.$watch('exclude',?val?=>?{
????pruneCache(this,?name?=>?!matches(val,?name))
??})
}
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)
??????}
????}
??}
}
keep-alive 在 mounted 會監(jiān)聽 include 和 exclude 的變化,屬性發(fā)生改變時調(diào)整緩存和 keys 的順序,最終調(diào)用的也是 pruneCacheEntry。
「小結(jié)」:cache 用于緩存組件,keys 存儲組件的 key,根據(jù)LRU策略來調(diào)整緩存組件。keep-alive 的 render 中最后會返回組件的 VNode,因此我們也可以得出一個結(jié)論,keep-alive 并非真的不會渲染,而是渲染的對象是包裹的子組件。
組件渲染流程
?溫馨提示:這部分內(nèi)容需要對
?render和patch過程有了解
渲染過程最主要的兩個過程就是 render 和 patch,在 render 之前還會有模板編譯,render 函數(shù)就是模板編譯后的產(chǎn)物,它負責構(gòu)建 VNode 樹,構(gòu)建好的 VNode 會傳遞給 patch,patch 根據(jù) VNode 的關(guān)系生成真實dom節(jié)點樹。
這張圖描述了 Vue 視圖渲染的流程:

VNode構(gòu)建完成后,最終會被轉(zhuǎn)換成真實dom,而 patch 是必經(jīng)的過程。為了更好的理解組件渲染的過程,假設(shè) keep-alive 包括的組件有A和B兩個組件,默認展示A組件。初始化渲染
組件在 patch 過程是會執(zhí)行 createComponent 來掛載組件的,A組件也不例外。
//?源碼位置:src/core/vdom/patch.js
function?createComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
??let?i?=?vnode.data
??if?(isDef(i))?{
????const?isReactivated?=?isDef(vnode.componentInstance)?&&?i.keepAlive
????if?(isDef(i?=?i.hook)?&&?isDef(i?=?i.init))?{
??????i(vnode,?false?/*?hydrating?*/)
????}
????//?after?calling?the?init?hook,?if?the?vnode?is?a?child?component
????//?it?should've?created?a?child?instance?and?mounted?it.?the?child
????//?component?also?has?set?the?placeholder?vnode's?elm.
????//?in?that?case?we?can?just?return?the?element?and?be?done.
????if?(isDef(vnode.componentInstance))?{
??????initComponent(vnode,?insertedVnodeQueue)
??????insert(parentElm,?vnode.elm,?refElm)
??????if?(isTrue(isReactivated))?{
????????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
??????}
??????return?true
????}
??}
}
isReactivated 標識組件是否重新激活。在初始化渲染時,A組件還沒有初始化構(gòu)造完成,componentInstance 還是 undefined。而A組件的 keepAlive 是 true,因為 keep-alive 作為父級包裹組件,會先于A組件掛載,也就是 kepp-alive 會先執(zhí)行 render 的過程,A組件被緩存起來,之后對插槽內(nèi)第一個組件(A組件)的 keepAlive 賦值為 true,不記得這個過程請看上面組件實現(xiàn)的代碼。所以此時的 isReactivated 是 false。
接著會調(diào)用 init 函數(shù)進行組件初始化,它是組件的一個鉤子函數(shù):
//?源碼位置:src/core/vdom/create-component.js
const?componentVNodeHooks?=?{
??init?(vnode:?VNodeWithData,?hydrating:?boolean):??boolean?{
????if?(
??????vnode.componentInstance?&&
??????!vnode.componentInstance._isDestroyed?&&
??????vnode.data.keepAlive
????)?{
??????//?kept-alive?components,?treat?as?a?patch
??????const?mountedNode:?any?=?vnode?//?work?around?flow
??????componentVNodeHooks.prepatch(mountedNode,?mountedNode)
????}?else?{
??????const?child?=?vnode.componentInstance?=?createComponentInstanceForVnode(
????????vnode,
????????activeInstance
??????)
??????child.$mount(hydrating???vnode.elm?:?undefined,?hydrating)
????}
??},
??//?...
}
createComponentInstanceForVnode 內(nèi)會 new Vue 構(gòu)造組件實例并賦值到 componentInstance,隨后調(diào)用 $mount 掛載組件。
回 createComponent,繼續(xù)走下面的邏輯:
if?(isDef(vnode.componentInstance))?{
??initComponent(vnode,?insertedVnodeQueue)
??insert(parentElm,?vnode.elm,?refElm)
??if?(isTrue(isReactivated))?{
????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
??}
??return?true
}
調(diào)用 initComponent 將 vnode.elm 賦值為真實dom,然后調(diào)用 insert 將組件的真實dom插入到父元素中。
所以在初始化渲染中,keep-alive 將A組件緩存起來,然后正常的渲染A組件。
緩存渲染
當切換到B組件,再切換回A組件時,A組件命中緩存被重新激活。
再次經(jīng)歷 patch 過程,keep-alive 是根據(jù)插槽獲取當前的組件,那么插槽的內(nèi)容又是如何更新實現(xiàn)緩存?
const?isRealElement?=?isDef(oldVnode.nodeType)
if?(!isRealElement?&&?sameVnode(oldVnode,?vnode))?{
??//?patch?existing?root?node
??patchVnode(oldVnode,?vnode,?insertedVnodeQueue,?null,?null,?removeOnly)
}
非初始化渲染時,patch 會調(diào)用 patchVnode 對比新舊節(jié)點。
//?源碼位置:src/core/vdom/patch.js
function?patchVnode?(
??oldVnode,
??vnode,
??insertedVnodeQueue,
??ownerArray,
??index,
??removeOnly
)?{
??//?...
??let?i
??const?data?=?vnode.data
??if?(isDef(data)?&&?isDef(i?=?data.hook)?&&?isDef(i?=?i.prepatch))?{
????i(oldVnode,?vnode)
??}
??//?...
}
patchVnode 內(nèi)會調(diào)用鉤子函數(shù) prepatch。
//?源碼位置:src/core/vdom/create-component.js
prepatch?(oldVnode:?MountedComponentVNode,?vnode:?MountedComponentVNode)?{
??const?options?=?vnode.componentOptions
??const?child?=?vnode.componentInstance?=?oldVnode.componentInstance
??updateChildComponent(
????child,
????options.propsData,?//?updated?props
????options.listeners,?//?updated?listeners
????vnode,?//?new?parent?vnode
????options.children?//?new?children
??)
},
updateChildComponent 就是更新的關(guān)鍵方法,它里面主要是更新實例的一些屬性:
//?源碼位置:src/core/instance/lifecycle.js
export?function?updateChildComponent?(
??vm:?Component,
??propsData:??Object,
??listeners:??Object,
??parentVnode:?MountedComponentVNode,
??renderChildren:??Array
)?{
??//?...
??//?Any?static?slot?children?from?the?parent?may?have?changed?during?parent's
??//?update.?Dynamic?scoped?slots?may?also?have?changed.?In?such?cases,?a?forced
??//?update?is?necessary?to?ensure?correctness.
??const?needsForceUpdate?=?!!(
????renderChildren?||???????????????//?has?new?static?slots
????vm.$options._renderChildren?||??//?has?old?static?slots
????hasDynamicScopedSlot
??)
??
??//?...
??
??//?resolve?slots?+?force?update?if?has?children
??if?(needsForceUpdate)?{
????vm.$slots?=?resolveSlots(renderChildren,?parentVnode.context)
????vm.$forceUpdate()
??}
}
Vue.prototype.$forceUpdate?=?function?()?{
??const?vm:?Component?=?this
??if?(vm._watcher)?{
????//?這里最終會執(zhí)行?vm._update(vm._render)
????vm._watcher.update()
??}
}
從注釋中可以看到 needsForceUpdate 是有插槽才會為 true,keep-alive 符合條件。首先調(diào)用 resolveSlots 更新 keep-alive 的插槽,然后調(diào)用 $forceUpdate 讓 keep-alive 重新渲染,再走一遍 render。因為A組件在初始化已經(jīng)緩存了,keep-alive 直接返回緩存好的A組件 VNode。VNode 準備好后,又來到了 patch 階段。
function?createComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
??let?i?=?vnode.data
??if?(isDef(i))?{
????const?isReactivated?=?isDef(vnode.componentInstance)?&&?i.keepAlive
????if?(isDef(i?=?i.hook)?&&?isDef(i?=?i.init))?{
??????i(vnode,?false?/*?hydrating?*/)
????}
????//?after?calling?the?init?hook,?if?the?vnode?is?a?child?component
????//?it?should've?created?a?child?instance?and?mounted?it.?the?child
????//?component?also?has?set?the?placeholder?vnode's?elm.
????//?in?that?case?we?can?just?return?the?element?and?be?done.
????if?(isDef(vnode.componentInstance))?{
??????initComponent(vnode,?insertedVnodeQueue)
??????insert(parentElm,?vnode.elm,?refElm)
??????if?(isTrue(isReactivated))?{
????????reactivateComponent(vnode,?insertedVnodeQueue,?parentElm,?refElm)
??????}
??????return?true
????}
??}
}
A組件再次經(jīng)歷 createComponent 的過程,調(diào)用 init。
const?componentVNodeHooks?=?{
??init?(vnode:?VNodeWithData,?hydrating:?boolean):??boolean?{
????if?(
??????vnode.componentInstance?&&
??????!vnode.componentInstance._isDestroyed?&&
??????vnode.data.keepAlive
????)?{
??????//?kept-alive?components,?treat?as?a?patch
??????const?mountedNode:?any?=?vnode?//?work?around?flow
??????componentVNodeHooks.prepatch(mountedNode,?mountedNode)
????}?else?{
??????const?child?=?vnode.componentInstance?=?createComponentInstanceForVnode(
????????vnode,
????????activeInstance
??????)
??????child.$mount(hydrating???vnode.elm?:?undefined,?hydrating)
????}
??},
}
這時將不再走 $mount 的邏輯,只調(diào)用 prepatch 更新實例屬性。所以在緩存組件被激活時,不會執(zhí)行 created 和 mounted 的生命周期函數(shù)。
回到 createComponent,此時的 isReactivated 為 true,調(diào)用 reactivateComponent:
function?reactivateComponent?(vnode,?insertedVnodeQueue,?parentElm,?refElm)?{
??let?i
??//?hack?for?#4339:?a?reactivated?component?with?inner?transition
??//?does?not?trigger?because?the?inner?node's?created?hooks?are?not?called
??//?again.?It's?not?ideal?to?involve?module-specific?logic?in?here?but
??//?there?doesn't?seem?to?be?a?better?way?to?do?it.
??let?innerNode?=?vnode
??while?(innerNode.componentInstance)?{
????innerNode?=?innerNode.componentInstance._vnode
????if?(isDef(i?=?innerNode.data)?&&?isDef(i?=?i.transition))?{
??????for?(i?=?0;?i?????????cbs.activate[i](emptyNode,?innerNode)
??????}
??????insertedVnodeQueue.push(innerNode)
??????break
????}
??}
??//?unlike?a?newly?created?component,
??//?a?reactivated?keep-alive?component?doesn't?insert?itself
??insert(parentElm,?vnode.elm,?refElm)
}
最后調(diào)用 insert 插入組件的dom節(jié)點,至此緩存渲染流程完成。
「小結(jié)」:組件首次渲染時,keep-alive 會將組件緩存起來。等到緩存渲染時,keep-alive 會更新插槽內(nèi)容,之后 $forceUpdate 重新渲染。這樣在 render 時就獲取到最新的組件,如果命中緩存則從緩存中返回 VNode。
總結(jié)
keep-alive 組件是抽象組件,在對應(yīng)父子關(guān)系時會跳過抽象組件,它只對包裹的子組件做處理,主要是根據(jù)LRU策略緩存組件 VNode,最后在 render 時返回子組件的 VNode。緩存渲染過程會更新 keep-alive 插槽,重新再 render 一次,從緩存中讀取之前的組件 VNode 實現(xiàn)狀態(tài)緩存。
