Vue源碼探究-組件的持久活躍
*本篇代碼位于vue/src/core/components/keep-alive.js
較新版本的Vue增加了一個(gè)內(nèi)置組件 keep-alive,用于存儲(chǔ)組件狀態(tài),即便失活也能保持現(xiàn)有狀態(tài)不變,切換回來的時(shí)候不會(huì)恢復(fù)到初始狀態(tài)。由此可知,路由切換的鉤子所觸發(fā)的事件處理是無法適用于 keep-alive 組件的,那如果需要根據(jù)失活與否來給予組件事件通知,該怎么辦呢?如前篇所述,keep-alive 組件有兩個(gè)特有的生命周期鉤子 activated 和 deactivated,用來響應(yīng)失活狀態(tài)的事件處理。
來看看 keep-alive 組件的實(shí)現(xiàn),代碼文件位于 components 里,目前入口文件里也只有 keep-alive 這一個(gè)內(nèi)置組件,但這個(gè)模塊的分離,會(huì)不會(huì)預(yù)示著官方將在未來開發(fā)更多具有特殊功能的內(nèi)置組件呢?
// 導(dǎo)入輔助函數(shù)
import { isRegExp, remove } from 'shared/util'
import { getFirstComponentChild } from 'core/vdom/helpers/index'
// 定義VNodeCache靜態(tài)類型
// 它是一個(gè)包含key名和VNode鍵值對(duì)的對(duì)象,可想而知它是用來存儲(chǔ)組件的
type VNodeCache = { [key: string]: ?VNode };
// 定義getComponentName函數(shù),用于獲取組件名稱,傳入組件配置對(duì)象
function getComponentName (opts: ?VNodeComponentOptions): ?string {
// 先嘗試獲取配置對(duì)象中定義的name屬性,或無則獲取標(biāo)簽名稱
return opts && (opts.Ctor.options.name || opts.tag)
}
// 定義matches函數(shù),進(jìn)行模式匹配,傳入匹配的模式類型數(shù)據(jù)和name屬性
function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
// 匹配數(shù)組模式
if (Array.isArray(pattern)) {
// 使用數(shù)組方法查找name,返回結(jié)果
return pattern.indexOf(name) > -1
} else if (typeof pattern === 'string') {
// 匹配字符串模式
// 將字符串轉(zhuǎn)換成數(shù)組查找name,返回結(jié)果
return pattern.split(',').indexOf(name) > -1
} else if (isRegExp(pattern)) {
// 匹配正則表達(dá)式
// 使用正則匹配name,返回結(jié)果
return pattern.test(name)
}
/* istanbul ignore next */
// 未匹配正確模式則返回false
return false
}
// 定義pruneCache函數(shù),修剪keep-alive組件緩存對(duì)象
// 接受keep-alive組件實(shí)例和過濾函數(shù)
function pruneCache (keepAliveInstance: any, filter: Function) {
// 獲取組件的cache,keys,_vnode屬性
const { cache, keys, _vnode } = keepAliveInstance
// 遍歷cache對(duì)象
for (const key in cache) {
// 獲取緩存資源
const cachedNode: ?VNode = cache[key]
// 如果緩存資源存在
if (cachedNode) {
// 獲取該資源的名稱
const name: ?string = getComponentName(cachedNode.componentOptions)
// 當(dāng)名稱存在 且不匹配緩存過濾時(shí)
if (name && !filter(name)) {
// 執(zhí)行修剪緩存資源操作
pruneCacheEntry(cache, key, keys, _vnode)
}
}
}
}
// 定義pruneCacheEntry函數(shù),修剪緩存條目
// 接受keep-alive實(shí)例的緩存對(duì)象和鍵名緩存對(duì)象,資源鍵名和當(dāng)前資源
function pruneCacheEntry (
cache: VNodeCache,
key: string,
keys: Array<string>,
current?: VNode
) {
// 檢查緩存對(duì)象里是否已經(jīng)有以key值存儲(chǔ)的資源
const cached = cache[key]
// 如果有舊資源并且沒有傳入新資源參數(shù)或新舊資源標(biāo)簽不同
if (cached && (!current || cached.tag !== current.tag)) {
// 銷毀該資源
cached.componentInstance.$destroy()
}
// 置空key鍵名存儲(chǔ)資源
cache[key] = null
// 移除key值的存儲(chǔ)
remove(keys, key)
}
// 定義模式匹配接收的數(shù)據(jù)類型
const patternTypes: Array<Function> = [String, RegExp, Array]
// 導(dǎo)出keep-alive組件實(shí)例的配置對(duì)象
export default {
// 定義組件名稱
name: 'keep-alive',
// 設(shè)置abstract屬性
abstract: true,
// 設(shè)置組件接收的屬性
props: {
// include用于包含模式匹配的資源,啟用緩存
include: patternTypes,
// exclude用于排除模式匹配的資源,不啟用緩存
exclude: patternTypes,
// 最大緩存數(shù)
max: [String, Number]
},
created () {
// 實(shí)例創(chuàng)建時(shí)定義cache屬性為空對(duì)象,用于存儲(chǔ)資源
this.cache = Object.create(null)
// 設(shè)置keys數(shù)組,用于存儲(chǔ)資源的key名
this.keys = []
},
destroyed () {
// 實(shí)例銷毀時(shí)一并銷毀存儲(chǔ)的資源并清空緩存對(duì)象
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted () {
// DOM加載完成后,觀察include和exclude屬性的變動(dòng)
// 回調(diào)執(zhí)行修改緩存對(duì)象的操作
this.$watch('include', val => {
pruneCache(this, name => matches(val, name))
})
this.$watch('exclude', val => {
pruneCache(this, name => !matches(val, name))
})
},
render () {
// 實(shí)例渲染函數(shù)
// 獲取keep-alive包含的子組件結(jié)構(gòu)
// keep-alive組件并不渲染任何真實(shí)DOM節(jié)點(diǎn),只渲染嵌套在其中的組件資源
const slot = this.$slots.default
// 將嵌套組件dom結(jié)構(gòu)轉(zhuǎn)化成虛擬節(jié)點(diǎn)
const vnode: VNode = getFirstComponentChild(slot)
// 獲取嵌套組件的配置對(duì)象
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
// 如果配置對(duì)象存在
if (componentOptions) {
// 檢查是否緩存的模式匹配
// check pattern
// 獲取嵌套組件名稱
const name: ?string = getComponentName(componentOptions)
// 獲取傳入keep-alive組件的include和exclude屬性
const { include, exclude } = this
// 如果有included,且該組件不匹配included中資源
// 或者有exclude。且該組件匹配exclude中的資源
// 則返回虛擬節(jié)點(diǎn),不繼續(xù)執(zhí)行緩存
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
return vnode
}
// 獲取keep-alive組件的cache和keys對(duì)象
const { cache, keys } = this
// 獲取嵌套組件虛擬節(jié)點(diǎn)的key
const key: ?string = vnode.key == null
// 同樣的構(gòu)造函數(shù)可能被注冊(cè)為不同的本地組件,所以cid不是判斷的充分條件
// 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
// 如果緩存對(duì)象里有以key值存儲(chǔ)的組件資源
if (cache[key]) {
// 設(shè)置當(dāng)前嵌套組件虛擬節(jié)點(diǎn)的componentInstance屬性
vnode.componentInstance = cache[key].componentInstance
// make current key freshest
// 從keys中移除舊key,添加新key
remove(keys, key)
keys.push(key)
} else {
// 緩存中沒有該資源,則直接存儲(chǔ)資源,并存儲(chǔ)key值
cache[key] = vnode
keys.push(key)
// 如果設(shè)置了最大緩存資源數(shù),從最開始的序號(hào)開始刪除存儲(chǔ)資源
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 設(shè)置該資源虛擬節(jié)點(diǎn)的keepAlive標(biāo)識(shí)
vnode.data.keepAlive = true
}
// 返回虛擬節(jié)點(diǎn)或dom節(jié)點(diǎn)
return vnode || (slot && slot[0])
}
}
keep-alive 組件的實(shí)現(xiàn)也就這百來行代碼,分為兩部分:第一部分是定義一些處理具體實(shí)現(xiàn)的函數(shù),比如修剪緩存對(duì)象存儲(chǔ)資源的函數(shù),匹配組件包含和過濾存儲(chǔ)的函數(shù);第二部分是導(dǎo)出一份 keep-alive 組件的應(yīng)用配置對(duì)象,仔細(xì)一下這跟我們?cè)趯?shí)際中使用的方式是一樣的,但這個(gè)組件具有已經(jīng)定義好的特殊功能
,就是緩存嵌套在它之中的組件資源,實(shí)現(xiàn)持久活躍。
那么實(shí)現(xiàn)原理是什么,在代碼里可以清楚得看到,這里是利用轉(zhuǎn)換組件真實(shí)DOM節(jié)點(diǎn)為虛擬節(jié)點(diǎn)將其存儲(chǔ)到 keep-alive 實(shí)例的 cache 對(duì)象中,另外也一并存儲(chǔ)了資源的 key 值方便查找,然后在渲染時(shí)檢測其是否符合緩存條件再進(jìn)行渲染。keep-alive 的實(shí)現(xiàn)就是以上這樣簡單。
最初一瞥此段代碼時(shí),不知所云。然而當(dāng)開始逐步分析代碼之后,才發(fā)現(xiàn)原來只是沒有仔細(xì)去看,誤以為很深?yuàn)W,由此可見,任何不用心的行為都不能直抵事物的本質(zhì),這是借由探索這一小部分代碼而得到的教訓(xùn)。因?yàn)樵趯?shí)際中有使用過這個(gè)功能,所以體會(huì)更深,有時(shí)候難免會(huì)踩到一些坑,看了源碼的實(shí)現(xiàn)之后,發(fā)現(xiàn)原來是自己使用方式不對(duì),所以了解所用輪子的實(shí)現(xiàn)還是很有必要的。