前言
大家好,我是林三心,用最通俗易懂講最難的知識點是我的座右銘,基礎是進階的前提是我的初心。
回想起來,我一開始寫作的時候就是寫Vue原始碼系列的,都收錄在我的掘金專欄Vue原始碼解析之中:
- 「Vue原始碼學習(一)」你不知道的-資料響應式原理
- Vue原始碼學習(二)」你不知道的-模板編譯原理
- 「Vue原始碼學習(三)」你不知道的-初次渲染原理
- 「Vue原始碼學習(四)」立志寫一篇人人都看的懂的computed,watch原理
- 「Vue原始碼學習(五)」面試官喜歡問的——Vue常用方法原始碼解析
- Vue原始碼學習」你想知道Vuex的實現原理嗎?
- 「Vue原始碼學習」你真的知道插槽Slot是怎麼“插”的嗎
- 15張圖,20分鐘吃透Diff演算法核心原理,我說的!!!
- 林三心畫了8張圖,最通俗易懂的Vue3響應式核心原理解析
- 7張圖,從零實現一個簡易版Vue-Router,太通俗易懂了!
今天,就給大家講講Vue中常用的元件 keep-alive
的基本原理吧!
場景
可能大家在平時的開發中會經常遇到這樣的場景:有一個可以進行篩選的列表頁 List.vue
,點選某一項時進入相應的詳情頁面,等到你從詳情頁返回 List.vue
時,發現列表頁居然重新整理了!剛剛的篩選條件都沒了!!!
keep-alive
是什麼?
keep-alive
是一個Vue全域性元件
keep-alive
本身不會渲染出來,也不會出現在父元件鏈中keep-alive
包裹動態元件時,會快取不活動的元件,而不是銷燬它們
怎麼用?
keep-alive
接收三個引數:
include
:可傳字串、正規表示式、陣列
,名稱匹配成功的元件會被快取exclude
:可傳字串、正規表示式、陣列
,名稱匹配成功的元件不會被快取max
:可傳數字
,限制快取元件的最大數量
include
和 exclude
,傳 陣列
情況居多
動態元件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<component :is="currentComponent"></component>
</keep-alive>
路由元件
<keep-alive :include="allowList" :exclude="noAllowList" :max="amount">
<router-view></router-view>
</keep-alive>
原始碼
元件基礎
前面說了, keep-alive
是一個 Vue全域性元件
,他接收三個引數:
include
:可傳字串、正規表示式、陣列
,名稱匹配成功的元件會被快取exclude
:可傳字串、正規表示式、陣列
,名稱匹配成功的元件不會被快取max
:可傳數字
,限制快取元件的最大數量,超過max
則按照LRU演算法
進行置換
順便說說 keep-alive
在各個生命週期裡都做了啥吧:
created
:初始化一個cache、keys
,前者用來存快取元件的虛擬dom集合,後者用來存快取元件的key集合mounted
:實時監聽include、exclude
這兩個的變化,並執行相應操作destroyed
:刪除掉所有快取相關的東西
之前說了,keep-alive
不會被渲染到頁面上,所以abstract
這個屬性至關重要!
// src/core/components/keep-alive.js
export default {
name: 'keep-alive',
abstract: true, // 判斷此元件是否需要在渲染成真實DOM
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created() {
this.cache = Object.create(null) // 建立物件來儲存 快取虛擬dom
this.keys = [] // 建立陣列來儲存 快取key
},
mounted() {
// 實時監聽include、exclude的變動
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
destroyed() {
for (const key in this.cache) { // 刪除所有的快取
pruneCacheEntry(this.cache, key, this.keys)
}
},
render() {
// 下面講
}
}
pruneCacheEntry函式
我們們上面實現的生命週期 destroyed
中,執行了 刪除所有快取
這個操作,而這個操作是通過呼叫 pruneCacheEntry
來實現的,那我們們來說說 pruneCacheEntry
裡做了啥吧
// src/core/components/keep-alive.js
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy() // 執行元件的destory鉤子函式
}
cache[key] = null // 設為null
remove(keys, key) // 刪除對應的元素
}
總結一下就是做了三件事:
- 1、遍歷集合,執行所有快取元件的
$destroy
方法 - 2、將
cache
對應key
的內容設定為null
- 3、刪除
keys
中對應的元素
render函式
以下稱include
為白名單,exclude
為黑名單render
函式裡主要做了這些事:
- 第一步:獲取到
keep-alive
包裹的第一個元件以及它的元件名稱
- 第二步:判斷此
元件名稱
是否能被白名單、黑名單
匹配,如果不能被白名單匹配 || 能被黑名單匹配
,則直接返回VNode
,不往下執行,如果不符合,則往下執行第三步
- 第三步:根據
元件ID、tag
生成快取key
,並在快取集合中查詢是否已快取過此元件。如果已快取過,直接取出快取元件,並更新快取key
在keys
中的位置(這是LRU演算法
的關鍵),如果沒快取過,則繼續第四步
- 第四步:分別在
cache、keys
中儲存此元件
以及他的快取key
,並檢查數量是否超過max
,超過則根據LRU演算法
進行刪除 - 第五步:將此元件例項的
keepAlive
屬性設定為true,這很重要哦,下面會講到的!
// src/core/components/keep-alive.js
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 // 定義元件的快取key
// 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) // 調整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])
}
渲染
我們們先來看看Vue一個元件是怎麼渲染的,我們們從 render
開始說:
render
:此函式會將元件轉成VNode
patch
:此函式在初次渲染時會直接渲染根據拿到的VNode
直接渲染成真實DOM
,第二次渲染開始就會拿VNode
會跟舊VNode
對比,打補丁(diff演算法對比發生在此階段),然後渲染成真實DOM
keep-alive本身渲染
剛剛說了, keep-alive
自身元件不會被渲染到頁面上,那是怎麼做到的呢?其實就是通過判斷元件例項上的 abstract
的屬性值,如果是 true
的話,就跳過該例項,該例項也不會出現在父級鏈上
// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
const options = vm.$options
// 找到第一個非abstract的父元件例項
let parent = options.parent
if (parent && !options.abstract) {
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
// ...
}
包裹元件渲染
我們們再來說說被 keep-alive
包裹著的元件是如何使用快取的吧。剛剛說了 VNode -> 真實DOM
是發生在 patch
的階段,而其實這也是要細分的: VNode -> 例項化 -> _update -> 真實DOM
,而元件使用快取的判斷就發生在 例項化
這個階段,而這個階段呼叫的是 createComponent
函式,那我們就來說說這個函式吧:
// 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 */)
}
if (isDef(vnode.componentInstance)) {
initComponent(vnode, insertedVnodeQueue)
insert(parentElm, vnode.elm, refElm) // 將快取的DOM(vnode.elm)插入父元素中
if (isTrue(isReactivated)) {
reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
}
return true
}
}
}
- 在第一次載入被包裹元件時,因為
keep-alive
的render
先於包裹元件載入之前執行,所以此時vnode.componentInstance
的值是undefined
,而keepAlive
是true
,則程式碼走到i(vnode, false /* hydrating */)
就不往下走了 - 再次訪問包裹元件時,
vnode.componentInstance
的值就是已經快取的元件例項,那麼會執行insert(parentElm, vnode.elm, refElm)
邏輯,這樣就直接把上一次的DOM插入到了父元素中。
結語
我是林三心,一個熱心的前端菜鳥程式設計師。如果你上進,喜歡前端,想學習前端,那我們們可以交朋友,一起摸魚哈哈,摸魚群