因爲
<keep-alive>
中的緩存優化遵循 LRU 原則,因此首先了解下緩存淘汰策略的相關介紹。
因爲緩存空間是有限的,因此不能無限制的進行數據存儲,當存儲容量達到一個閥值時,就會形成內存溢出,所以在進行數據緩存時,就要根據狀況對緩存進行優化,清除一些可能不會再用到的數據。因此根據緩存淘汰的機制不一樣,經常使用的有如下三種:html
咱們經過記錄數據使用的時間,當緩存大小即將溢出時,優先清楚離當前時間最遠的數據。vue
以時間做爲參考,若是數據最近被訪問過,那麼未來被訪問的概率會更高,若是以一個數組去記錄數據,當有一數據被訪問時,該數據會被移動到數組的末尾,代表最近被使用過,當緩存溢出時,會刪除數組的頭部數據,即將最不頻繁使用的數據移除。(keep-alive 的優化處理)node
以次數做爲參考,用次數去標記數據使用頻率,次數最少的會在緩存溢出時被淘汰。react
<keep-alive>
簡單示例首先咱們看一個動態組件使用 <keep-alive>
的例子)。api
<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" class="tab" ></component> </keep-alive> </div>
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>
那就意味着,當選項卡 Posts
、 Archive
在來回切換時,所對應的組件實例會被緩存起來,因此當再次切換到 Posts
選項時,其對應的組件 tab-posts
會從緩存中獲取,計數器 count
也會保留上一次的狀態。緩存
<keep-alive>
緩存及優化處理就此,咱們看完 <keep-alive>
的簡單示例以後,讓咱們一塊兒來分析下源碼中它是如何進行組件緩存和緩存優化處理的。app
vue 在模板 -> AST -> render() -> vnode -> 真實Dom
這個轉化過程當中,會進入 patch
階段,在patch
階段,會調用 createElm
函數中會將 vnode
轉化爲真實 dom
。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()
是組件實例化的過程,組件實例化無非就是一系列選項合併,初始化事件,生命週期等初始化操做。
<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); } }
回顧以前提到的首次渲染階段,會調用 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>
組件的首次渲染、組件緩存和緩存優化處理相關的實現就到這裏。
最後記住這幾個點:
<keep-alive>
是 vue 內置組件,在源碼定義中,也具備本身的組件選項如 data
、 method
、 computed
、 props
等。<keep-alive>
具備抽象組件標識 abstract
,一般會與動態組件一同使用。<keep-alive>
包裹動態組件時,會緩存不活動的組件實例,將它們停用,而不是銷燬它們。<keep-alive>
緩存的組件會觸發 activated
或 deactivated
生命週期鉤子。<keep-alive>
會緩存組件實例的 vnode
對象 ,和真實 dom
節點,因此會有 max
屬性設置。<keep-alive>
不會在函數式組件中正常工做,由於它們沒有緩存實例。