從源碼角度來(lái)回答keep-alive組件的緩存原理
今天開(kāi)門見(jiàn)山地聊一下面試中被問(wèn)到的一個(gè)問(wèn)題:keep-alive組件的緩存原理。
官方API介紹和用法
- <keep-alive> 包裹動(dòng)態(tài)組件時(shí),會(huì)緩存不活動(dòng)的組件實(shí)例,而不是銷毀它們。
- 和 <transition> 相似,<keep-alive> 是一個(gè)抽象組件:它自身不會(huì)渲染一個(gè) DOM 元素,也不會(huì)出現(xiàn)在組件的父組件鏈中。
- 當(dāng)組件在 <keep-alive> 內(nèi)被切換,它的 activated 和 deactivated 這兩個(gè)生命周期鉤子函數(shù)將會(huì)被對(duì)應(yīng)執(zhí)行。
官網(wǎng)的例子是 tab 切換保存了用戶的操作,實(shí)際中還可能遇到從列表頁(yè)跳轉(zhuǎn)去了詳情頁(yè),再跳轉(zhuǎn)回列表頁(yè)需要保存用戶進(jìn)行過(guò)的篩選操作,這就需要用到 <keep-alive>,這樣也能避免重新渲染,提高頁(yè)面性能。
用法及props的講解
// keep-alive組件搭配動(dòng)態(tài)組件的用法,還要其他的用法參見(jiàn)官網(wǎng) <keep-alive include="['componentNameA', 'componentNameB']" exclude="'componentNameC'" :max="10"> <component :is="view"></component> </keep-alive>
- include - 字符串或正則表達(dá)式或數(shù)組,name匹配上的組件會(huì)被緩存
- exclude - 字符串或正則表達(dá)式或數(shù)組,name匹配上的組件都不會(huì)被緩存
- max - 字符串或數(shù)字,緩存組件實(shí)例的最大數(shù),最久沒(méi)有被訪問(wèn)的實(shí)例會(huì)被銷毀掉
注意:
- <keep-alive> 只渲染其直系的一個(gè)組件,因此若在 <keep-alive> 中用 v-for,則其不會(huì)工作,若多條件判斷有多個(gè)符合條件也同理不工作。
- include 和 exclude 匹配時(shí),首先檢查組件的 name 選項(xiàng),若 name 選項(xiàng)不可用,則匹配它的局部注冊(cè)名稱 (即父組件 components 選項(xiàng)的鍵值)。匿名組件不能被匹配。
- <keep-alive> 不會(huì)在函數(shù)式組件中正常工作,因?yàn)樗鼈儧](méi)有緩存實(shí)例。
源碼解讀
先貼一張?jiān)创a圖

總共125行,收起來(lái)一看其實(shí)東西也比較少。前面是引入一些需要用到的方法,然后定義了一些 keep-alive 組件自己會(huì)用到的一些方法,最后就是向外暴露一個(gè) name 為 keep-alive 的組件選項(xiàng),這些選項(xiàng)除了 abstract 外,其他的我們都比較熟悉了,其中, render 函數(shù)就是緩存原理最重要的部分,也能看出 keep-alive 組件是一個(gè)函數(shù)式組件。
// isRegExp函數(shù)判斷是不是正則表達(dá)式,remove移除數(shù)組中的某一個(gè)成員
// getFirstComponentChild獲取VNode數(shù)組的第一個(gè)有效組件
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
type VNodeCache = { [key: string]: ?VNode }; // 緩存組件VNode的緩存類型
// 通過(guò)組件的name或組件tag來(lái)獲取組件名(上面注意的第二點(diǎn))
function getComponentName (opts: ?VNodeComponentOptions): ?string {
return opts && (opts.Ctor.options.name || opts.tag)
}
// 判斷include或exclude跟組件的name是否匹配成功
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
if (Array.isArray(pattern)) {
return pattern.indexOf(name) > -1 // include或exclude是數(shù)組的情況
} else if (typeof pattern === 'string') {
return pattern.split(',').indexOf(name) > -1 // include或exclude是字符串的情況
} else if (isRegExp(pattern)) {
return pattern.test(name) // include或exclude是正則表達(dá)式的情況
}
return false // 都沒(méi)匹配上(上面注意的二三點(diǎn))
}
// 銷毀緩存
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance // keep-alive組件實(shí)例
for (const key in cache) {
const cachedNode: ?VNode = cache[key] // 已經(jīng)被緩存的組件
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
// 若name存在且不能跟include或exclude匹配上就銷毀這個(gè)已經(jīng)緩存的組件
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
// 銷毀緩存的入口
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key] // 被緩存過(guò)的組件
// “已經(jīng)被緩存的組件是否繼續(xù)被緩存” 有變動(dòng)時(shí)
// 若組件被緩存命中過(guò)且當(dāng)前組件不存在或緩存命中組件的tag和當(dāng)前組件的tag不相等
if (cached && (!current || cached.tag !== current.tag)) {
// 說(shuō)明現(xiàn)在這個(gè)組件不需要被繼續(xù)緩存,銷毀這個(gè)組件實(shí)例
cached.componentInstance.$destroy()
}
cache[key] = null // 把緩存中這個(gè)組件置為null
remove(keys, key) // 把這個(gè)組件的key移除出keys數(shù)組
}
// 示例類型
const patternTypes: Array<Function> = [String, RegExp, Array]
// 向外暴露keep-alive組件的一些選項(xiàng)
export default {
name: 'keep-alive', // 組件名
abstract: true, // keep-alive是抽象組件
// 用keep-alive組件時(shí)傳入的三個(gè)props
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null) // 存儲(chǔ)需要緩存的組件
this.keys = [] // 存儲(chǔ)每個(gè)需要緩存的組件的key,即對(duì)應(yīng)this.cache對(duì)象中的鍵值
},
// 銷毀keep-alive組件的時(shí)候,對(duì)緩存中的每個(gè)組件執(zhí)行銷毀
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
// keep-alive組件掛載時(shí)監(jiān)聽(tīng)include和exclude的變化,條件滿足時(shí)就銷毀已緩存的組件
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
// 重點(diǎn)來(lái)了
render () {
const slot = this.$slots.default // keep-alive組件的默認(rèn)插槽
const vnode: VNode = getFirstComponentChild(slot) // 獲取默認(rèn)插槽的第一個(gè)有效組件
// 如果vnode存在就取vnode的選項(xiàng)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
//獲取第一個(gè)有效組件的name
const name: ?string = getComponentName(componentOptions)
const { include, exclude } = this // props傳遞來(lái)的include和exclude
if (
// 若include存在且name不存在或name未匹配上
(include && (!name || !matches(include, name))) ||
// 若exclude存在且name存在或name匹配上
(exclude && name && matches(exclude, name))
) {
return vnode // 說(shuō)明不用緩存,直接返回這個(gè)組件進(jìn)行渲染
}
// 匹配上就需要進(jìn)行緩存操作
const { cache, keys } = this // keep-alive組件的緩存組件和緩存組件對(duì)應(yīng)的key
// 獲取第一個(gè)有效組件的key
const key: ?string = vnode.key == null
// 同一個(gè)構(gòu)造函數(shù)可以注冊(cè)為不同的本地組件
// 所以僅靠cid是不夠的,進(jìn)行拼接一下
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
// 如果這個(gè)組件命中緩存
if (cache[key]) {
// 這個(gè)組件的實(shí)例用緩存中的組件實(shí)例替換
vnode.componentInstance = cache[key].componentInstance
// 更新當(dāng)前key在keys中的位置
remove(keys, key) // 把當(dāng)前key從keys中移除
keys.push(key) // 再放到keys的末尾
} else {
// 如果沒(méi)有命中緩存,就把這個(gè)組件加入緩存中
cache[key] = vnode
keys.push(key) // 把這個(gè)組件的key放到keys的末尾
// 如果緩存中的組件個(gè)數(shù)超過(guò)傳入的max,銷毀緩存中的LRU組件
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true // 設(shè)置這個(gè)組件的keepAlive屬性為true
}
// 若第一個(gè)有效的組件存在,但其componentOptions不存在,就返回這個(gè)組件進(jìn)行渲染
// 或若也不存在有效的第一個(gè)組件,但keep-alive組件的默認(rèn)插槽存在,就返回默認(rèn)插槽的第一個(gè)組件進(jìn)行渲染
return vnode || (slot && slot[0])
}
}
補(bǔ)充:
上面關(guān)于刪除第一個(gè)舊緩存組件和更新緩存組件 key 的順序,其實(shí)是用到了LRU緩存淘汰策略:
LRU全稱Least Recently Used,最近最少使用的意思,是一種內(nèi)存管理算法。
這種算法基于一種假設(shè):長(zhǎng)期不用的數(shù)據(jù),在未來(lái)被用到的幾率也很小,因此,當(dāng)數(shù)據(jù)所占內(nèi)存達(dá)到一定閾值,可以移除掉最近最少使用的。
總結(jié)
簡(jiǎn)單總結(jié)為:
keep-alive 組件在渲染的時(shí)候,會(huì)根據(jù)傳入的 include 和 exclude 來(lái)匹配 keep-alive 包裹的命名組件,未匹配上就直接返回這個(gè)命名組件進(jìn)行渲染,若匹配上就進(jìn)行緩存操作:若緩存中已有這個(gè)組件,就替換其實(shí)例,并更新這個(gè)組件的 key 在 keys 中的位置;若緩存中沒(méi)有這個(gè)組件,就把這個(gè)組件放入 keep-alive 組件的緩存 cache 中,并把這個(gè)組件的 key 放入 keys 中,由于在 mounted 的時(shí)候有對(duì) include 和 exclude 進(jìn)行監(jiān)聽(tīng),因此,后續(xù)這兩個(gè)屬性值發(fā)生變化時(shí),會(huì)再次判斷是否滿足條件而進(jìn)行組件銷毀。
到此這篇關(guān)于從源碼角度來(lái)回答keep-alive組件的緩存原理的文章就介紹到這了,更多相關(guān)keep-alive組件緩存內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
vue中ref和e.target的區(qū)別以及ref用法
這篇文章主要介紹了vue中ref和e.target的區(qū)別以及ref用法,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
Vue 樣式切換及三元判斷樣式關(guān)聯(lián)操作
這篇文章主要介紹了Vue 樣式切換及三元判斷樣式關(guān)聯(lián)操作,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過(guò)來(lái)看看吧2020-08-08
Vue源碼解析之Template轉(zhuǎn)化為AST的實(shí)現(xiàn)方法
這篇文章主要介紹了Vue源碼解析之Template轉(zhuǎn)化為AST的實(shí)現(xiàn)方法,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2018-12-12
vue使用jsencrypt實(shí)現(xiàn)rsa前端加密的操作代碼
這篇文章主要介紹了vue使用jsencrypt實(shí)現(xiàn)rsa前端加密,本文通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-09-09
vue預(yù)覽本地pdf文件方法之vue-pdf組件使用
這篇文章主要介紹了vue預(yù)覽本地pdf文件方法之vue-pdf組件使用方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-03-03
關(guān)于vue v-for 循環(huán)問(wèn)題(一行顯示四個(gè),每一行的最右邊那個(gè)計(jì)算屬性)
這篇文章主要介紹了關(guān)于vue v-for 循環(huán)問(wèn)題(一行顯示四個(gè),每一行的最右邊那個(gè)計(jì)算屬性),需要的朋友可以參考下2018-09-09

