Keep-alive 緩存及其緩存優化原理
緩存淘汰策略
由於緩存空間是有限的,所以不能無限制的進行數據存儲,當存儲容量達到一個閥值時,就會造成內存溢出,因此在進行數據緩存時,就要根據情況對緩存進行優化,清除一些可能不會再用到的數據。
-
FIFO(fisrt-in-fisrt-out)- 先進先出策略
我們通過記錄數據使用的時間,當緩存大小即將溢出時,優先清楚離當前時間最遠的數據。
-
LRU (least-recently-used)- 最近最少使用策略
以時間作爲參考,如果數據最近被訪問過,那麼將來被訪問的幾率會更高,如果以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,表明最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。(keep-alive 的優化處理)
-
LFU (least-frequently-used)- 計數最少策略
以次數作爲參考,用次數去標記數據使用頻率,次數最少的會在緩存溢出時被淘汰。
<keep-alive>
簡單示例
Vue.component('tab-posts', {
data: function () {
return {
count: 0
}
},
template: `
<div class="posts-tab">
<button @click="count++">Click Me</button>
<p>{{count}}</p>
</div>`
})
Vue.component('tab-archive', {
template: '<div>Archive component</div>'
})
new Vue({
el: '#dynamic-component-demo',
data: {
currentTab: 'Posts',
},
computed: {
currentTabComponent: function () {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
我們可以看到,動態組件外層包裹着 <keep-alve>
標籤。
<keep-alive>
<component
v-bind:is="currentTabComponent"
class="tab"
></component>
</keep-alive>
<keep-alive>
緩存及優化處理
就此,我們看完 <keep-alive>
的簡單示例之後,讓我們一起來分析下源碼中它是如何進行組件緩存和緩存優化處理的。
首次渲染
vue 在模板 -> AST -> render() -> vnode -> 真實Dom
這個轉化過程中,會進入 patch
階段,在patch
階段,會調用 createElm
函數中會將 vnode
轉化爲真實 dom
。
function createPatchFunction (backend) {
...
//生成真實dom
function createElm (
vnode,
insertedVnodeQueue,
parentElm,
refElm,
nested,
ownerArray,
index
) {
// 返回 true 代表爲 vnode 爲組件 vnode,將停止接下來的轉換過程
if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
return;
}
...
}
}
在轉化節點的過程中,因爲 <keep-alive>
的 vnode
會視爲組件 vnode
,因此一開始會調用 createComponent()
函數,createComponent()
會執行組件初始化內部鉤子 init()
, 對組件進行初始化和實例化。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated 用來判斷組件是否緩存
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 執行組件初始化的內部鉤子 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);
// 將真實 dom 添加到父節點,insert 操作 dom api
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
<keep-alive>
組件通過調用內部鉤子 init()
方法進行初始化操作。
注:源碼中通過函數
installComponentHooks()
可追蹤到內部鉤子的定義對象componentVNodeHooks
。
// inline hooks to be invoked on component VNodes during patch
var componentVNodeHooks = {
init: function init (vnode, hydrating) {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) {
// kept-alive components, treat as a patch
var mountedNode = vnode; // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode);
} else {
// 第一次運行時,vnode.componentInstance 不存在 ,vnode.data.keepAlive 不存在
// 將組件實例化,並賦值給 vnode 的 componentInstance 屬性
var child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
);
// 進行掛載
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
}
},
// prepatch 是 patch 過程的核心步驟
prepatch: function prepatch (oldVnode, vnode) { ... },
insert: function insert (vnode) { ... },
destroy: function destroy (vnode) { ... }
};
第一次執行時,很明顯組件 vnode
沒有 componentInstance
屬性,vnode.data.keepAlive
也沒有值,所以會調用 createComponentInstanceForVnode()
將組件進行實例化並將組件實例賦值給 vnode
的componentInstance
屬性,最後執行組件實例的 $mount
方法進行實例掛載。
createComponentInstanceForVnode()
是組件實例化的過程,組件實例化無非就是一系列選項合併,初始化事件,生命週期等初始化操作。
緩存 vnode 節點
...
// 進行掛載
child.$mount(hydrating ? vnode.elm : undefined, hydrating);
...
掛載 $mount
階段會調用 mountComponent()
函數進行 vm._update(vm._render(), hydrating);
操作。
Vue.prototype.$mount = function (el, hydrating) {
el = el && inBrowser ? query(el) : undefined;
return mountComponent(this, el, hydrating)
};
function mountComponent (vm, el, hydrating) {
vm.$el = el;
...
callHook(vm, 'beforeMount');
var updateComponent;
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
...
} else {
updateComponent = function () {
// vm._render() 會根據數據的變化爲組件生成新的 Vnode 節點
// vm._update() 最終會爲 Vnode 生成真實 DOM 節點
vm._update(vm._render(), hydrating);
}
}
...
return vm
}
而 vm._render()
函數最終會調用組件選項中的 render()
函數,進行渲染。
function renderMixin (Vue) {
...
Vue.prototype._render = function () {
var vm = this;
var ref = vm.$options;
var render = ref.render;
...
try {
...
// 調用組件的 render 函數
vnode = render.call(vm._renderProxy, vm.$createElement);
}
...
return vnode
};
}
由於keep-alive
是一個內置組件,因此也擁有自己的 render()
函數,所以讓我們一起來看下 render()
函數的具體實現。
var KeepAlive = {
...
props: {
include: patternTypes, // 名稱匹配的組件會被緩存,對外暴露 include 屬性 api
exclude: patternTypes, // 名稱匹配的組件不會被緩存,對外暴露 exclude 屬性 api
max: [String, Number] // 可以緩存的組件最大個數,對外暴露 max 屬性 api
},
created: function created () {},
destroyed: function destroyed () {},
mounted: function mounted () {},
// 在渲染階段,進行緩存的存或者取
render: function render () {
// 首先拿到 keep-alve 下插槽的默認值 (包裹的組件)
var slot = this.$slots.default;
// 獲取第一個 vnode 節點
var vnode = getFirstComponentChild(slot); // # 3802 line
// 拿到第一個子組件實例
var componentOptions = vnode && vnode.componentOptions;
// 如果 keep-alive 第一個組件實例不存在
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 根據匹配規則返回 vnode
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
// 獲取本地組件唯一key
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名重新存入數組最末端
} else {
// 進行緩存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根據組件名與 max 進行比較
if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
// 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 爲緩存組件打上標誌
vnode.data.keepAlive = true;
}
// 返回 vnode
return vnode || (slot && slot[0])
}
};
從上可得知,在 keep-alive
的源碼定義中, render()
階段會緩存 vnode
和組件名稱 key
等操作。
-
首先會判斷是否存在緩存,如果存在,則直接從緩存中獲取組件的實例,並進行緩存優化處理(稍後會介紹到)。
-
如果不存在緩存,會將
vnode
作爲值存入cache
對象對應的key
中。還會將組件名稱存入keys
數組中。
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名重新存入數組最末端
} else {
// 進行緩存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根據組件名與 max 進行比較
if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
// 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
緩存真實 DOM
回顧之前提到的首次渲染階段,會調用 createComponent()
函數, createComponent()
會執行組件初始化內部鉤子 init()
,對組件進行初始化和實例化等操作。
function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
var i = vnode.data;
if (isDef(i)) {
// isReactivated 用來判斷組件是否緩存
var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
if (isDef(i = i.hook) && isDef(i = i.init)) {
// 執行組件初始化的內部鉤子 init()
i(vnode, false /* hydrating */);
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue);
// 將真實 dom 添加到父節點,insert 操作 dom api
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
}
}
createComponet()
函數還會我們通過 vnode.componentInstance
拿到了 <keep-alive>
組件的實例,然後執行 initComponent()
,initComponent()
函數的作用就是將真實的 dom
保存再 vnode
中。
...
if (isDef(vnode.componentInstance)) {
// 其中的一個作用就是保存真實 dom 到 vnode 中
initComponent(vnode, insertedVnodeQueue);
// 將真實 dom 添加到父節點,(insert 操作 dom api)
insert(parentElm, vnode.elm, refElm);
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
}
return true
}
...
function initComponent (vnode, insertedVnodeQueue) {
if (isDef(vnode.data.pendingInsert)) {
insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert);
vnode.data.pendingInsert = null;
}
// 保存真實 dom 節點到 vnode
vnode.elm = vnode.componentInstance.$el;
...
}
緩存優化處理 LRU
在文章開頭,我們介紹了三種緩存優化策略(它們各有優劣),而在 vue 中對 <keep-alive>
的緩存優化處理的實現上,便用到了上述的 LRU
緩存策略 。
上面介紹到,<keep-alive>
組件在存取緩存的過程中,是在渲染階段進行的,所以我們回過頭來看 render()
函數的實現。
var KeepAlive = {
...
props: {
include: patternTypes, // 名稱匹配的組件會被緩存,對外暴露 include 屬性 api
exclude: patternTypes, // 名稱匹配的組件不會被緩存,對外暴露 exclude 屬性 api
max: [String, Number] // 可以緩存的組件最大個數,對外暴露 max 屬性 api
},
// 創建節點生成緩存對象
created: function created () {
this.cache = Object.create(null); // 緩存 vnode
this.keys = []; // 緩存組件名
},
// 在渲染階段,進行緩存的存或者取
render: function render () {
// 首先拿到 keep-alve 下插槽的默認值 (包裹的組件)
var slot = this.$slots.default;
// 獲取第一個 vnode 節點
var vnode = getFirstComponentChild(slot); // # 3802 line
// 拿到第一個子組件實例
var componentOptions = vnode && vnode.componentOptions;
// 如果 keep-alive 第一個組件實例不存在
if (componentOptions) {
// check pattern
var name = getComponentName(componentOptions);
var ref = this;
var include = ref.include;
var exclude = ref.exclude;
// 根據匹配規則返回 vnode
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
var ref$1 = this;
var cache = ref$1.cache;
var keys = ref$1.keys;
var key = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
// 獲取本地組件唯一key
? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
: vnode.key;
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance;
// make current key freshest
// 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名重新存入數組最末端
} else {
// 進行緩存
cache[key] = vnode;
keys.push(key);
// prune oldest entry
// 根據組件名與 max 進行比較
if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
// 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
// 爲緩存組件打上標誌
vnode.data.keepAlive = true;
}
// 返回 vnode
return vnode || (slot && slot[0])
}
};
<keep-alive>
組件會在創建階段生成緩存對象,在渲染階段對組件進行緩存,並進行緩存優化。我們重點來看下段代碼。
if (cache[key]) {
...
// 使用 LRU 最近最少緩存策略,將命中的 key 從緩存數組中刪除,並將當前最新 key 存入緩存數組的末尾
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名重新存入數組最末端
} else {
// 進行緩存
cache[key] = vnode;
keys.push(key);
// 根據組件名與 max 進行比較
if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
// 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
}
從註釋中我們可以得知,當 keep-alive
被激活時(觸發 activated
鉤子),會執行 remove(keys, key)
函數,從緩存數組中 keys
刪除已存在的組件,之後會進行 push
操作,將當前組件名重新存入 keys
數組的最末端,正好符合 LRU
。
LRU:以時間作爲參考,如果數據最近被訪問過,那麼將來被訪問的幾率會更高,如果以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,表明最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。
remove(keys, key); // 刪除命中已存在的組件
keys.push(key); // 將當前組件名重新存入數組最末端
function remove (arr, item) {
if (arr.length) {
var index = arr.indexOf(item);
if (index > -1) {
return arr.splice(index, 1)
}
}
}
至此,我們可以回過頭看我們上邊的 <keep-alive>
示例,示例中包含 tab-posts
、tab-archive
兩個組件,通過 component
的 is
屬性動態渲染。當 tab
來回切換時,會將兩個組件的 vnode
和組件名稱存入緩存中,如下。
keys = ['tab-posts', 'tab-archive']
cache = {
'tab-posts': tabPostsVnode,
'tab-archive': tabArchiveVnode
}
假如,當再次激活到 tabPosts
組件時,由於命中了緩存,會調用源碼中的 remove()
方法,從緩存數組中 keys
把tab-posts
刪除,之後會使用 push
方法將 tab-posts
推到末尾。這時緩存結果變爲:
keys = ['tab-archive', 'tab-posts']
cache = {
'tab-posts': tabPostsVnode,
'tab-archive': tabArchiveVnode
}
現在我們可以得知,keys
用來緩存組件名是用來記錄緩存數據的。那麼當緩存溢出時, <keep-alive>
又是如何 處理的呢?
我們可以通過
max
屬性來限制最多可以緩存多少組件實例。
在上面源碼中的 render()
階段,還有一個 pruneCacheEntry(cache, keys[0], keys, this._vnode)
函數,根據 LRU
淘汰策略,會在緩存溢出時,刪除緩存中的頭部數據,所以會將 keys[0]
傳入pruneCacheEntry()
。
if (this.max && keys.length > parseInt(this.max)) { // 超出組件緩存最大數的限制
// 執行 pruneCacheEntry 對最少訪問數據(數組的第一項)進行刪除
pruneCacheEntry(cache, keys[0], keys, this._vnode);
}
pruneCacheEntry()
具體邏輯如下:
-
首先,通過
cached$$1 = cache[key]
獲取頭部數據對應的值vnode
,執行cached$$1.componentInstance.$destroy()
將組件實例銷燬。 -
其次,執行
cache[key] = null
清空組件對應的緩存節點。 -
最後,執行
remove(keys, key)
刪除緩存中的頭部數據keys[0]
。
至此,關於 <keep-alive>
組件的首次渲染、組件緩存和緩存優化處理相關的實現就到這裏。
最後
最後記住這幾個點:
-
<keep-alive>
是 vue 內置組件,在源碼定義中,也具有自己的組件選項如data
、method
、computed
、props
等。 -
<keep-alive>
具有抽象組件標識abstract
,通常會與動態組件一同使用。 -
<keep-alive>
包裹動態組件時,會緩存不活動的組件實例,將它們停用,而不是銷燬它們。 -
被
<keep-alive>
緩存的組件會觸發activated
或deactivated
生命週期鉤子。 -
<keep-alive>
會緩存組件實例的vnode
對象 ,和真實dom
節點,所以會有max
屬性設置。 -
<keep-alive>
不會在函數式組件中正常工作,因爲它們沒有緩存實例。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/CV6rz73IA-tTI5JAPnJM5Q