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

現(xiàn)在緩存最大只允許存3個(gè)組件,ABC三個(gè)組件依次進(jìn)入緩存,沒有任何問題 當(dāng)D組件被訪問時(shí),內(nèi)存空間不足,A是最早進(jìn)入也是最舊的組件,所以A組件從緩存中刪除,D組件加入到最新的位置 當(dāng)B組件被再次訪問時(shí),由于B還在緩存中,B移動(dòng)到最新的位置,其他組件相應(yīng)的往后一位 當(dāng)E組件被訪問時(shí),內(nèi)存空間不足,C變成最久未使用的組件,C組件從緩存中刪除,E組件加入到最新的位置
keep-alive 緩存機(jī)制便是根據(jù)LRU策略來設(shè)置緩存組件新鮮度,將很久未訪問的組件從緩存中刪除。了解完緩存機(jī)制,接下來進(jìn)入源碼,看看keep-alive組件是如何實(shí)現(xiàn)的。
組件實(shí)現(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 實(shí)際是一個(gè)抽象組件,只對(duì)包裹的子組件做處理,并不會(huì)和子組件建立父子關(guān)系,也不會(huì)作為節(jié)點(diǎn)渲染到頁面上。在組件開頭就設(shè)置 abstract 為 true,代表該組件是一個(gè)抽象組件。
//?源碼位置: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)系的呢?在初始化階段會(huì)調(diào)用 initLifecycle,里面判斷父級(jí)是否為抽象組件,如果是抽象組件,就選取抽象組件的上一級(jí)作為父級(jí),忽略與抽象組件和子組件之間的層級(jí)關(guān)系。
回到 keep-alive 組件,組件是沒有編寫 template 模板,而是由 render 函數(shù)決定渲染結(jié)果。
const?slot?=?this.$slots.default
const?vnode:?VNode?=?getFirstComponentChild(slot)
如果 keep-alive 存在多個(gè)子元素,keep-alive 要求同時(shí)只有一個(gè)子元素被渲染。所以在開頭會(huì)獲取插槽內(nèi)的子元素,調(diào)用 getFirstComponentChild 獲取到第一個(gè)子元素的 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
}
接著判斷當(dāng)前組件是否符合緩存條件,組件名與include不匹配或與exclude匹配都會(huì)直接退出并返回 VNode,不走緩存機(jī)制。
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
匹配條件通過會(huì)進(jìn)入緩存機(jī)制的邏輯,如果命中緩存,從 cache 中獲取緩存的實(shí)例設(shè)置到當(dāng)前的組件上,并調(diào)整 key 的位置將其放到最后。如果沒命中緩存,將當(dāng)前 VNode 緩存起來,并加入當(dāng)前組件的 key。如果緩存組件的數(shù)量超出 max 的值,即緩存空間不足,則調(diào)用 pruneCacheEntry 將最舊的組件從緩存中刪除,即 keys[0] 的組件。之后將組件的 keepAlive 標(biāo)記為 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 負(fù)責(zé)將組件從緩存中刪除,它會(huì)調(diào)用組件 $destroy 方法銷毀組件實(shí)例,緩存組件置空,并移除對(duì)應(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 會(huì)監(jiān)聽 include 和 exclude 的變化,屬性發(fā)生改變時(shí)調(diào)整緩存和 keys 的順序,最終調(diào)用的也是 pruneCacheEntry。
小結(jié):cache 用于緩存組件,keys 存儲(chǔ)組件的 key,根據(jù)LRU策略來調(diào)整緩存組件。keep-alive 的 render 中最后會(huì)返回組件的 VNode,因此我們也可以得出一個(gè)結(jié)論,keep-alive 并非真的不會(huì)渲染,而是渲染的對(duì)象是包裹的子組件。
組件渲染流程
溫馨提示:這部分內(nèi)容需要對(duì)
render和patch過程有了解
渲染過程最主要的兩個(gè)過程就是 render 和 patch,在 render 之前還會(huì)有模板編譯,render 函數(shù)就是模板編譯后的產(chǎn)物,它負(fù)責(zé)構(gòu)建 VNode 樹,構(gòu)建好的 VNode 會(huì)傳遞給 patch,patch 根據(jù) VNode 的關(guān)系生成真實(shí)dom節(jié)點(diǎn)樹。
這張圖描述了 Vue 視圖渲染的流程:?
?
VNode構(gòu)建完成后,最終會(huì)被轉(zhuǎn)換成真實(shí)dom,而 patch 是必經(jīng)的過程。為了更好的理解組件渲染的過程,假設(shè) keep-alive 包括的組件有A和B兩個(gè)組件,默認(rèn)展示A組件。
初始化渲染
組件在 patch 過程是會(huì)執(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 標(biāo)識(shí)組件是否重新激活。在初始化渲染時(shí),A組件還沒有初始化構(gòu)造完成,componentInstance 還是 undefined。而A組件的 keepAlive 是 true,因?yàn)?keep-alive 作為父級(jí)包裹組件,會(huì)先于A組件掛載,也就是 kepp-alive 會(huì)先執(zhí)行 render 的過程,A組件被緩存起來,之后對(duì)插槽內(nèi)第一個(gè)組件(A組件)的 keepAlive 賦值為 true,不記得這個(gè)過程請(qǐng)看上面組件實(shí)現(xiàn)的代碼。所以此時(shí)的 isReactivated 是 false。
接著會(huì)調(diào)用 init 函數(shù)進(jìn)行組件初始化,它是組件的一個(gè)鉤子函數(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)會(huì) new Vue 構(gòu)造組件實(shí)例并賦值到 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 賦值為真實(shí)dom,然后調(diào)用 insert 將組件的真實(shí)dom插入到父元素中。
所以在初始化渲染中,keep-alive 將A組件緩存起來,然后正常的渲染A組件。
緩存渲染
當(dāng)切換到B組件,再切換回A組件時(shí),A組件命中緩存被重新激活。
再次經(jīng)歷 patch 過程,keep-alive 是根據(jù)插槽獲取當(dāng)前的組件,那么插槽的內(nèi)容又是如何更新實(shí)現(xiàn)緩存?
const?isRealElement?=?isDef(oldVnode.nodeType)
if?(!isRealElement?&&?sameVnode(oldVnode,?vnode))?{
??//?patch?existing?root?node
??patchVnode(oldVnode,?vnode,?insertedVnodeQueue,?null,?null,?removeOnly)
}
非初始化渲染時(shí),patch 會(huì)調(diào)用 patchVnode 對(duì)比新舊節(jié)點(diǎn)。
//?源碼位置: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)會(huì)調(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)鍵方法,它里面主要是更新實(shí)例的一些屬性:
//?源碼位置: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)?{
????//?這里最終會(huì)執(zhí)行?vm._update(vm._render)
????vm._watcher.update()
??}
}
從注釋中可以看到 needsForceUpdate 是有插槽才會(huì)為 true,keep-alive 符合條件。首先調(diào)用 resolveSlots 更新 keep-alive 的插槽,然后調(diào)用 $forceUpdate 讓 keep-alive 重新渲染,再走一遍 render。因?yàn)锳組件在初始化已經(jīng)緩存了,keep-alive 直接返回緩存好的A組件 VNode。VNode 準(zhǔn)備好后,又來到了 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)
????}
??},
}
這時(shí)將不再走 $mount 的邏輯,只調(diào)用 prepatch 更新實(shí)例屬性。所以在緩存組件被激活時(shí),不會(huì)執(zhí)行 created 和 mounted 的生命周期函數(shù)。
回到 createComponent,此時(shí)的 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é)點(diǎn),至此緩存渲染流程完成。
小結(jié):組件首次渲染時(shí),keep-alive 會(huì)將組件緩存起來。等到緩存渲染時(shí),keep-alive 會(huì)更新插槽內(nèi)容,之后 $forceUpdate 重新渲染。這樣在 render 時(shí)就獲取到最新的組件,如果命中緩存則從緩存中返回 VNode。
總結(jié)
keep-alive 組件是抽象組件,在對(duì)應(yīng)父子關(guān)系時(shí)會(huì)跳過抽象組件,它只對(duì)包裹的子組件做處理,主要是根據(jù)LRU策略緩存組件 VNode,最后在 render 時(shí)返回子組件的 VNode。緩存渲染過程會(huì)更新 keep-alive 插槽,重新再 render 一次,從緩存中讀取之前的組件 VNode 實(shí)現(xiàn)狀態(tài)緩存。
