聊聊vue中的keep-alive

一、什么是keep-alive?
官方介紹就是:<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)被切換時(shí),它的 mounted 和 unmounted 生命周期鉤子不會(huì)被調(diào)用,取而代之的是 activated 和 deactivated。
簡(jiǎn)單理解就是說我們可以把一些不常變動(dòng)的組件或者需要緩存的組件用<keep-alive>包裹起來,這樣<keep-alive>就會(huì)幫我們把組件保存在內(nèi)存中,而不是直接的銷毀,這樣做可以保留組件的狀態(tài)或避免多次重新渲染,以提高頁面性能
二、使用用法
我們先根據(jù)官方文檔來回顧一下<keep-alive>組件的具體用法,如下:
<keep-alive>組件可接收三個(gè)屬性:
-
Props: -
include-string | RegExp | Array。只有名稱匹配的組件會(huì)被緩存。 -
exclude-string | RegExp | Array。任何名稱匹配的組件都不會(huì)被緩存。 -
max-number | string。最多可以緩存多少組件實(shí)例。 -
用法:
include和excludeprop 允許組件有條件地緩存。二者都可以用逗號(hào)分隔字符串、正則表達(dá)式或一個(gè)數(shù)組來表示:
<!-- 逗號(hào)分隔字符串 -->
<keep-alive include="a,b">
<component :is="view"></component>
</keep-alive>
<!-- regex (使用 `v-bind`) -->
<keep-alive :include="/a|b/">
<component :is="view"></component>
</keep-alive>
<!-- Array (使用 `v-bind`) -->
<keep-alive :include="['a', 'b']">
<component :is="view"></component>
</keep-alive>
匹配首先檢查組件自身的 name 選項(xiàng),如果 name 選項(xiàng)不可用,則匹配它的局部注冊(cè)名稱 (父組件 components 選項(xiàng)的鍵值)。匿名組件不能被匹配。
max表示最多可以緩存多少組件實(shí)例。一旦這個(gè)數(shù)字達(dá)到了,在新實(shí)例被創(chuàng)建之前,已緩存組件中最久沒有被訪問的實(shí)例會(huì)被銷毀掉。
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
在這里簡(jiǎn)單介紹一個(gè)日常項(xiàng)目中有可能出現(xiàn)的場(chǎng)景并使用keep-alive來實(shí)現(xiàn)按需控制緩存 場(chǎng)景:當(dāng)我們從首頁–>列表頁–>商品詳情頁–>返回到列表頁(需要緩存)–>返回到首頁(需要緩存)–>再次進(jìn)入列表頁(不需要緩存)
-
在路由meta對(duì)象里定義兩個(gè)值:
keepAlive:這個(gè)路由是否需要緩存deepth:代表頁面之間的前進(jìn)后退的層級(jí)關(guān)系
{
path: '*',
name: 'Home',
component: () => import(/* webpackPreload: true */ '@/views/home'),
meta: {
keepAlive: true,
deepth: 1
}
},
{
path: '/list',
name: 'list',
component: () => import('@/views/list'),
meta: {
keepAlive: true,
deepth: 2
}
},
{
path: '/detail',
name: 'Detail',
component: () => import('@/views/detail'),
meta: {
keepAlive: true,
deepth: 3
}
},
-
監(jiān)聽路由動(dòng)態(tài)控制需要緩存的值
//3x版本router-view不允許直接寫在keep-alive里面,需注意
<template>
<div id="app">
<keep-alive :include="include">
<router-view v-if="$route.meta.keepAlive" />
</keep-alive>
<router-view v-if="!$route.meta.keepAlive" />
</div>
</template>
export default {
data() {
return {
include: []
};
},
watch: {
$route(to, from) {
// 如果要to(進(jìn)入)的頁面是需要keepAlive緩存的,把name push進(jìn)include數(shù)組中
if (to.meta.keepAlive) {
!this.include.includes(to.name) && this.include.push(to.name);
}
// 如果 要 form(離開) 的頁面是 keepAlive緩存的,
// 再根據(jù) deepth 來判斷是前進(jìn)還是后退
// 如果是后退:
if (from.meta.keepAlive && to.meta.deepth < from.meta.deepth) {
const index = this.include.indexOf(from.name);
index !== -1 && this.include.splice(index, 1);
}
}
}
};
以上場(chǎng)景在通過監(jiān)聽路由,動(dòng)態(tài)的設(shè)置了在第一次進(jìn)入并回退回來時(shí)的緩存實(shí)現(xiàn),并在第二次進(jìn)入時(shí)重新開始進(jìn)行新一輪緩存設(shè)置,實(shí)現(xiàn)動(dòng)態(tài)控制緩存。
三、實(shí)現(xiàn)
<keep-alive>組件的定義位于源碼的 src/core/components/keep-alive.js 文件中,本文參考:https://unpkg.com/browse/[email protected]/src/core/components/keep-alive.js,感興趣的可以自行查看,下面只展示部分代碼。
const patternTypes: Array<Function> = [String, RegExp, Array]
export default {
name: 'keep-alive',
abstract: true,
props: {
include: patternTypes,
exclude: patternTypes,
max: [String, Number]
},
created () {
this.cache = Object.create(null)
this.keys = []
},
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
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
// 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)
} 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])
}
}
開始我們先從created鉤子開始進(jìn)行分析:
created:
在 created 鉤子函數(shù)里定義并初始化了兩個(gè)屬性: this.cache 和 this.keys。
created () {
this.cache = Object.create(null)
this.keys = []
}
this.cache是一個(gè)對(duì)象,用來存儲(chǔ)需要緩存的組件。
this.keys是一個(gè)數(shù)組,用來存儲(chǔ)每個(gè)需要緩存的組件的key,即對(duì)應(yīng)this.cache對(duì)象中的鍵值。
destroyed:
當(dāng)<keep-alive>組件被銷毀時(shí),此時(shí)會(huì)調(diào)用destroyed鉤子函數(shù),在該鉤子函數(shù)里會(huì)遍歷this.cache對(duì)象,然后將那些被緩存的并且當(dāng)前沒有處于被渲染狀態(tài)的組件都銷毀掉并將其從this.cache對(duì)象中刪除。如下:
destroyed () {
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
}
上面用到了pruneCacheEntry函數(shù):
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
const cached = cache[key]
/* 判斷當(dāng)前沒有處于被渲染狀態(tài)的組件,將其銷毀*/
if (cached && (!current || cached.tag !== current.tag)) {
cached.componentInstance.$destroy()
}
cache[key] = null
remove(keys, key)
}
mounted:
在mounted鉤子函數(shù)中觀測(cè) include 和 exclude 的變化,如下:
mounted () {
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
}
如果include 或exclude 發(fā)生了變化,即表示定義需要緩存的組件的規(guī)則或者不需要緩存的組件的規(guī)則發(fā)生了變化,那么就執(zhí)行pruneCache函數(shù)
function pruneCache (keepAliveInstance: any, filter: Function) {
const { cache, keys, _vnode } = keepAliveInstance
for (const key in cache) {
const cachedNode: ?VNode = cache[key]
if (cachedNode) {
const name: ?string = getComponentName(cachedNode.componentOptions)
if (name && !filter(name)) {
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
在該函數(shù)內(nèi)對(duì)this.cache對(duì)象進(jìn)行遍歷,取出每一項(xiàng)的name值,用其與新的緩存規(guī)則進(jìn)行匹配,如果匹配不上,則表示在新的緩存規(guī)則下該組件已經(jīng)不需要被緩存,則調(diào)用pruneCacheEntry函數(shù)將這個(gè)已經(jīng)不需要緩存的組件實(shí)例先銷毀掉,然后再將其從this.cache對(duì)象中刪除。
render:
<keep-alive> 為一個(gè)函數(shù)式組件。執(zhí)行組件渲染的時(shí)候,就會(huì)執(zhí)行到這個(gè) render 函數(shù)
render () {
/* 獲取默認(rèn)插槽中的第一個(gè)組件節(jié)點(diǎn) */
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 獲取該組件節(jié)點(diǎn)的名稱 */
const name: ?string = getComponentName(componentOptions)
/* 如果name與include規(guī)則不匹配或者與exclude規(guī)則匹配則表示不緩存,直接返回vnode */
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
/*-----需要走緩存-----*/
const { cache, keys } = this
/* 獲取組件的key */
const key: ?string = vnode.key == null
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
/* 如果命中緩存,則直接從緩存中拿 vnode 的組件實(shí)例 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest 調(diào)整該組件key的順序,將其從原來的地方刪掉并重新放在最后一個(gè)
remove(keys, key)
keys.push(key)
} else {
/* 如果沒有命中緩存,則將其設(shè)置進(jìn)緩存 */
cache[key] = vnode
keys.push(key)
// prune oldest entry 如果配置了max并且緩存的長(zhǎng)度超過了this.max,則從緩存中刪除第一個(gè)
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
vnode.data.keepAlive = true
}
return vnode || (slot && slot[0])
}
-
獲取默認(rèn)插槽中的第一個(gè)組件節(jié)點(diǎn)。 由于我們也是在 <keep-alive>標(biāo)簽內(nèi)部寫 DOM,所以可以先獲取到它的默認(rèn)插槽,然后再獲取到它的第一個(gè)子節(jié)點(diǎn)。<keep-alive>只處理第一個(gè)子元素,所以一般和它搭配使用的有component動(dòng)態(tài)組件或者是router-view。 -
獲取該組件節(jié)點(diǎn)的名稱,然后用組件名稱跟 include、exclude中的匹配規(guī)則去匹配,如果組件名稱與include規(guī)則不匹配或者與exclude規(guī)則匹配,則表示不緩存該組件,直接返回這個(gè)組件的vnode,否則的話走下一步緩存 -
獲取組件的 key值:拿到key值后去this.cache對(duì)象中去尋找是否有該值,如果有則表示該組件有緩存,即命中緩存,直接從緩存中拿vnode的組件實(shí)例,此時(shí)重新調(diào)整該組件key的順序,將其從原來的地方刪掉并重新放在this.keys中最后一個(gè)。沒有繼續(xù)下一步 -
表明該組件還沒有被緩存過,則以該組件的 key為鍵,組件vnode為值,將其存入this.cache中,并且把key存入this.keys中。此時(shí)再判斷this.keys中緩存組件的數(shù)量是否超過了設(shè)置的最大緩存數(shù)量值this.max,如果超過了,則把第一個(gè)緩存組件刪掉 -
最后設(shè)置 vnode.data.keepAlive = true,最后將vnode返回
總結(jié)
上面介紹了Vue中的內(nèi)置組件<keep-alive>組件以及<keep-alive>組件的具體用法。同時(shí)也分析了<keep-alive>組件的一些內(nèi)部原理,底下則是個(gè)人對(duì)于keep-alive的運(yùn)行的一些個(gè)人理解總結(jié): keep-alive主要作用是緩存vnode,大概可以分為三個(gè)運(yùn)行階段
-
初始未存在緩存階段,在 created鉤子定義了用于保存vnode的cache對(duì)象以及保存緩存了的vnode列表keys,用于數(shù)據(jù)的存儲(chǔ)。mounted鉤子觀測(cè)監(jiān)聽include和exclude的變化,進(jìn)行緩存vnode的變化更新,最后調(diào)用render進(jìn)行組件vnode的第一次緩存設(shè)置。 因?yàn)榫彺嬉恍┚唧w業(yè)務(wù)功能的組件vnode對(duì)于我們來說,什么時(shí)候開始緩存、何時(shí)銷毀以及何時(shí)運(yùn)行render重新刷新,開發(fā)者是沒有vnode的直接控制能力,所以需要定義一些屬性include、exclude、max來進(jìn)行一個(gè)判斷,用于更準(zhǔn)確的對(duì)需要緩存的vnode進(jìn)行控制處理。 -
已緩存的更新階段,調(diào)用 render函數(shù)直接從cache對(duì)象返回已緩存的vnode,避免了多次的重新渲染,來提高頁面性能。 -
銷毀階段, destroyed鉤子定義組件銷毀時(shí)清除那些被緩存的并且當(dāng)前沒有處于被渲染狀態(tài)的組件。銷毀組件時(shí),對(duì)于緩存的vnode對(duì)象,不清除的話應(yīng)該也會(huì)造成一些內(nèi)存上的占用或者內(nèi)存泄漏的問題,所以在銷毀時(shí)需要進(jìn)行一個(gè)清除緩存的操作。
以上就是關(guān)于keep-alive的相關(guān)內(nèi)容,希望每個(gè)看完的同學(xué)都有自己收獲,完結(jié) ??????
