緩存淘汰策略
由於 <keep-alive> 中的緩存優化遵循 LRU 原則,所以首先了解下緩存淘汰策略的相關介紹。
由於緩存空間是有限的,所以不能無限制的進行數據存儲,當存儲容量達到一個閥值時,就會造成內存溢出,因此在進行數據緩存時,就要根據情況對緩存進行優化,清除一些可能不會再用到的數據。所以根據緩存淘汰的機制不同,常用的有以下三種:
FIFO(fisrt-in-fisrt-out)- 先進先出策略
我們通過記錄數據使用的時間,當緩存大小即將溢出時,優先清楚離當前時間最遠的數據。
LRU (least-recently-used)- 最近最少使用策略
以時間作為參考,如果數據最近被訪問過,那么將來被訪問的幾率會更高,如果以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,表明最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。(keep-alive 的優化處理)
LFU (least-frequently-used)- 計數最少策略
以次數作為參考,用次數去標記數據使用頻率,次數最少的會在緩存溢出時被淘汰。
<keep-alive> 簡單示例
首先我們看一個動態組件使用 <keep-alive> 的例子)。
<div id="dynamic-component-demo"> <button v-on:click="currentTab = 'Posts'">Posts</button> <button v-on:click="currentTab = 'Archive'">Archive</button> <keep-alive> <component v-bind:is="currentTabComponent" ></component> </keep-alive> </div>
vue.component('tab-posts', { data: function () { return { count: 0 } }, template: ` <div> <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>
那就意味着,當選項卡 Posts 、 Archive 在來回切換時,所對應的組件實例會被緩存起來,所以當再次切換到 Posts 選項時,其對應的組件 tab-posts 會從緩存中獲取,計數器 count 也會保留上一次的狀態。
<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 節點
<keep-alive> 在執行組件實例化之后會進行組件的掛載(如上代碼所示)。
...
// 進行掛載 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; ... }
緩存優化處理
在文章開頭,我們介紹了三種緩存優化策略(它們各有優劣),而在 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> 組件的首次渲染、組件緩存和緩存優化處理相關的實現就到這里。
vi設計http://www.maiqicn.com 辦公資源網站大全https://www.wode007.com
最后
最后記住這幾個點:
<keep-alive> 是 vue 內置組件,在源碼定義中,也具有自己的組件選項如 data 、 method 、 computed 、 props 等。
<keep-alive> 具有抽象組件標識 abstract,通常會與動態組件一同使用。
<keep-alive> 包裹動態組件時,會緩存不活動的組件實例,將它們停用,而不是銷毀它們。
被 <keep-alive> 緩存的組件會觸發 activated 或 deactivated 生命周期鈎子。
<keep-alive> 會緩存組件實例的 vnode 對象 ,和真實 dom 節點,所以會有 max 屬性設置。
<keep-alive> 不會在函數式組件中正常工作,因為它們沒有緩存實例。