聊聊vue中的keep-alive

一、什么是keep-alive?
官方介紹就是:<keep-alive> 包裹動態(tài)組件時,會緩存不活動的組件實例,而不是銷毀它們。和 <transition> 相似,<keep-alive> 是一個抽象組件:它自身不會渲染一個 DOM 元素,也不會出現(xiàn)在組件的父組件鏈中。
當(dāng)組件在 <keep-alive> 內(nèi)被切換時,它的 mounted 和 unmounted 生命周期鉤子不會被調(diào)用,取而代之的是 activated 和 deactivated。
簡單理解就是說我們可以把一些不常變動的組件或者需要緩存的組件用<keep-alive>包裹起來,這樣<keep-alive>就會幫我們把組件保存在內(nèi)存中,而不是直接的銷毀,這樣做可以保留組件的狀態(tài)或避免多次重新渲染,以提高頁面性能
二、使用用法
我們先根據(jù)官方文檔來回顧一下<keep-alive>組件的具體用法,如下:
<keep-alive>組件可接收三個屬性:
-
Props: -
include-string | RegExp | Array。只有名稱匹配的組件會被緩存。 -
exclude-string | RegExp | Array。任何名稱匹配的組件都不會被緩存。 -
max-number | string。最多可以緩存多少組件實例。 -
用法:
include和excludeprop 允許組件有條件地緩存。二者都可以用逗號分隔字符串、正則表達(dá)式或一個數(shù)組來表示:
<!-- 逗號分隔字符串 -->
<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 選項,如果 name 選項不可用,則匹配它的局部注冊名稱 (父組件 components 選項的鍵值)。匿名組件不能被匹配。
max表示最多可以緩存多少組件實例。一旦這個數(shù)字達(dá)到了,在新實例被創(chuàng)建之前,已緩存組件中最久沒有被訪問的實例會被銷毀掉。
<keep-alive :max="10">
<component :is="view"></component>
</keep-alive>
在這里簡單介紹一個日常項目中有可能出現(xiàn)的場景并使用keep-alive來實現(xiàn)按需控制緩存 場景:當(dāng)我們從首頁–>列表頁–>商品詳情頁–>返回到列表頁(需要緩存)–>返回到首頁(需要緩存)–>再次進(jìn)入列表頁(不需要緩存)
-
在路由meta對象里定義兩個值:
keepAlive:這個路由是否需要緩存deepth:代表頁面之間的前進(jìn)后退的層級關(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)聽路由動態(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);
}
}
}
};
以上場景在通過監(jiān)聽路由,動態(tài)的設(shè)置了在第一次進(jìn)入并回退回來時的緩存實現(xiàn),并在第二次進(jìn)入時重新開始進(jìn)行新一輪緩存設(shè)置,實現(xiàn)動態(tài)控制緩存。
三、實現(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ù)里定義并初始化了兩個屬性: this.cache 和 this.keys。
created () {
this.cache = Object.create(null)
this.keys = []
}
this.cache是一個對象,用來存儲需要緩存的組件。
this.keys是一個數(shù)組,用來存儲每個需要緩存的組件的key,即對應(yīng)this.cache對象中的鍵值。
destroyed:
當(dāng)<keep-alive>組件被銷毀時,此時會調(diào)用destroyed鉤子函數(shù),在該鉤子函數(shù)里會遍歷this.cache對象,然后將那些被緩存的并且當(dāng)前沒有處于被渲染狀態(tài)的組件都銷毀掉并將其從this.cache對象中刪除。如下:
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ù)中觀測 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)對this.cache對象進(jìn)行遍歷,取出每一項的name值,用其與新的緩存規(guī)則進(jìn)行匹配,如果匹配不上,則表示在新的緩存規(guī)則下該組件已經(jīng)不需要被緩存,則調(diào)用pruneCacheEntry函數(shù)將這個已經(jīng)不需要緩存的組件實例先銷毀掉,然后再將其從this.cache對象中刪除。
render:
<keep-alive> 為一個函數(shù)式組件。執(zhí)行組件渲染的時候,就會執(zhí)行到這個 render 函數(shù)
render () {
/* 獲取默認(rèn)插槽中的第一個組件節(jié)點 */
const slot = this.$slots.default
const vnode: VNode = getFirstComponentChild(slot)
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
if (componentOptions) {
/* 獲取該組件節(jié)點的名稱 */
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 的組件實例 */
if (cache[key]) {
vnode.componentInstance = cache[key].componentInstance
// make current key freshest 調(diào)整該組件key的順序,將其從原來的地方刪掉并重新放在最后一個
remove(keys, key)
keys.push(key)
} else {
/* 如果沒有命中緩存,則將其設(shè)置進(jìn)緩存 */
cache[key] = vnode
keys.push(key)
// prune oldest entry 如果配置了max并且緩存的長度超過了this.max,則從緩存中刪除第一個
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)插槽中的第一個組件節(jié)點。 由于我們也是在 <keep-alive>標(biāo)簽內(nèi)部寫 DOM,所以可以先獲取到它的默認(rèn)插槽,然后再獲取到它的第一個子節(jié)點。<keep-alive>只處理第一個子元素,所以一般和它搭配使用的有component動態(tài)組件或者是router-view。 -
獲取該組件節(jié)點的名稱,然后用組件名稱跟 include、exclude中的匹配規(guī)則去匹配,如果組件名稱與include規(guī)則不匹配或者與exclude規(guī)則匹配,則表示不緩存該組件,直接返回這個組件的vnode,否則的話走下一步緩存 -
獲取組件的 key值:拿到key值后去this.cache對象中去尋找是否有該值,如果有則表示該組件有緩存,即命中緩存,直接從緩存中拿vnode的組件實例,此時重新調(diào)整該組件key的順序,將其從原來的地方刪掉并重新放在this.keys中最后一個。沒有繼續(xù)下一步 -
表明該組件還沒有被緩存過,則以該組件的 key為鍵,組件vnode為值,將其存入this.cache中,并且把key存入this.keys中。此時再判斷this.keys中緩存組件的數(shù)量是否超過了設(shè)置的最大緩存數(shù)量值this.max,如果超過了,則把第一個緩存組件刪掉 -
最后設(shè)置 vnode.data.keepAlive = true,最后將vnode返回
總結(jié)
上面介紹了Vue中的內(nèi)置組件<keep-alive>組件以及<keep-alive>組件的具體用法。同時也分析了<keep-alive>組件的一些內(nèi)部原理,底下則是個人對于keep-alive的運行的一些個人理解總結(jié): keep-alive主要作用是緩存vnode,大概可以分為三個運行階段
-
初始未存在緩存階段,在 created鉤子定義了用于保存vnode的cache對象以及保存緩存了的vnode列表keys,用于數(shù)據(jù)的存儲。mounted鉤子觀測監(jiān)聽include和exclude的變化,進(jìn)行緩存vnode的變化更新,最后調(diào)用render進(jìn)行組件vnode的第一次緩存設(shè)置。 因為緩存一些具體業(yè)務(wù)功能的組件vnode對于我們來說,什么時候開始緩存、何時銷毀以及何時運行render重新刷新,開發(fā)者是沒有vnode的直接控制能力,所以需要定義一些屬性include、exclude、max來進(jìn)行一個判斷,用于更準(zhǔn)確的對需要緩存的vnode進(jìn)行控制處理。 -
已緩存的更新階段,調(diào)用 render函數(shù)直接從cache對象返回已緩存的vnode,避免了多次的重新渲染,來提高頁面性能。 -
銷毀階段, destroyed鉤子定義組件銷毀時清除那些被緩存的并且當(dāng)前沒有處于被渲染狀態(tài)的組件。銷毀組件時,對于緩存的vnode對象,不清除的話應(yīng)該也會造成一些內(nèi)存上的占用或者內(nèi)存泄漏的問題,所以在銷毀時需要進(jìn)行一個清除緩存的操作。
以上就是關(guān)于keep-alive的相關(guān)內(nèi)容,希望每個看完的同學(xué)都有自己收獲,完結(jié) ??????
