??<keep-alive>是Vue.js的一個(gè)內(nèi)置組件,可以使被包含的組件保留狀態(tài)或避免重新渲染。下面來分析源碼runtime-core/src/components/KeepAlive.ts的實(shí)現(xiàn)原理。
??在setup方法中會(huì)創(chuàng)建一個(gè)緩存容器和緩存的key列表,其代碼如下:
setup(props: KeepAliveProps, { slots }: SetupContext) {
// keep-alive組件的上下文對(duì)象
const instance = getCurrentInstance()!
// KeepAlive communicates with the instantiated renderer via the
// ctx where the renderer passes in its internals,
// and the KeepAlive instance exposes activate/deactivate implementations.
// The whole point of this is to avoid importing KeepAlive directly in the
// renderer to facilitate tree-shaking.
const sharedContext = instance.ctx as KeepAliveContext
// if the internal renderer is not registered, it indicates that this is server-side rendering,
// for KeepAlive, we just need to render its children
if (__SSR__ && !sharedContext.renderer) {
return () => {
const children = slots.default && slots.default()
return children && children.length === 1 ? children[0] : children
}
}
/* 緩存對(duì)象 */
const cache: Cache = new Map()
const keys: Keys = new Set()
// 替換內(nèi)容
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 處理props改變
patch(
...
)
...
}
// 替換內(nèi)容
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
...
}
}
??<keep-alive>自己實(shí)現(xiàn)了render方法,并沒有使用Vue內(nèi)置的render方法(經(jīng)過<template>內(nèi)容提取、轉(zhuǎn)換AST、render字符串等一系列過程),在執(zhí)行<keep-alive>組件渲染時(shí),就會(huì)執(zhí)行這個(gè)render方法:
return () => {
pendingCacheKey = null
if (!slots.default) {
return null
}
// 得到插槽中的第一個(gè)組件
const children = slots.default()
const rawVNode = children[0]
if (children.length > 1) {
if (__DEV__) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
} else if (
!isVNode(rawVNode) ||
(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
) {
current = null
return rawVNode
}
let vnode = getInnerChild(rawVNode)
const comp = vnode.type as ConcreteComponent
// for async components, name check should be based in its loaded
// inner component if available
// 獲取組件名稱,優(yōu)先獲取組件的name字段
const name = getComponentName(
isAsyncWrapper(vnode)
? (vnode.type as ComponentOptions).__asyncResolved || {}
: comp
)
// name不在include中或者exclude中,則直接返回vnode(沒有存取緩存)
const { include, exclude, max } = props
if (
(include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))
) {
current = vnode
return rawVNode
}
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
// clone vnode if it's reused because we are going to mutate it
if (vnode.el) {
vnode = cloneVNode(vnode)
if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
rawVNode.ssContent = vnode
}
}
// #1513 it's possible for the returned vnode to be cloned due to attr
// fallthrough or scopeId, so the vnode here may not be the final vnode
// that is mounted. Instead of caching it directly, we store the pending
// key and cache `instance.subTree` (the normalized vnode) in
// beforeMount/beforeUpdate hooks.
pendingCacheKey = key
// 如果已經(jīng)緩存了,則直接從緩存中獲取組件實(shí)例給vnode,若還未緩存,則先進(jìn)行緩存
if (cachedVNode) {
// copy over mounted state
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 執(zhí)行transition
if (vnode.transition) {
// recursively update transition hooks on subTree
setTransitionHooks(vnode, vnode.transition!)
}
// 設(shè)置shapeFlag標(biāo)志位,為了避免執(zhí)行組件mounted方法
// avoid vnode being mounted as fresh
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
// make this key the freshest
// 重新設(shè)置一下key保證最新
keys.delete(key)
keys.add(key)
} else {
keys.add(key)
// prune oldest entry
// 當(dāng)超出max值時(shí),清除緩存
if (max && keys.size > parseInt(max as string, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
// avoid vnode being unmounted
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = vnode
return isSuspense(rawVNode.type) ? rawVNode : vnode
}
??在上面的代碼中,當(dāng)緩存的個(gè)數(shù)超過max(默認(rèn)值為10)的值時(shí),就會(huì)清除舊的數(shù)據(jù),這其中就包含<keep-alive>的緩存更新策略,其遵循了LRU(Least Rencently Used)算法。
1. LRU算法
??LRU算法根據(jù)數(shù)據(jù)的歷史訪問記錄來淘汰數(shù)據(jù),其核心思想是“如果數(shù)據(jù)最近被訪問過,那么將來被訪問的概率也更高”。利用這個(gè)思路,我們可以對(duì)<keep-alive>中緩存的組件數(shù)據(jù)進(jìn)行刪除和更新,其算法的核心實(shí)現(xiàn)如下:

上面的代碼中,主要利用map來存儲(chǔ)緩存數(shù)據(jù),利用map.keyIterator.next()來找到最久沒有使用的key對(duì)應(yīng)的數(shù)據(jù),從而對(duì)緩存進(jìn)行刪除和更新。
2. 緩存VNode對(duì)象
??在render方法中,<keep-alive>并不是直接緩存的DOM節(jié)點(diǎn),而是Vue中內(nèi)置的VNode對(duì)象,VNode經(jīng)過render方法后,會(huì)被替換成真正的DOM內(nèi)容。首先通過slots.default().children[0]獲取第一個(gè)子組件,獲取該組件的name。接下來會(huì)將這個(gè)name通過include與exclude屬性進(jìn)行匹配,若匹配不成功(說明不需要進(jìn)行緩存),則不進(jìn)行任何操作直接返回VNode。需要注意的是,<keep-alive>只會(huì)處理它的第一個(gè)子組件,所以如果給<keep-alive>設(shè)置多個(gè)子組件,是無法生效的。
??<keep-alive>還有一個(gè)watch方法,用來監(jiān)聽include和exclude的改變,代碼如下:
// prune cache on include/exclude prop change
watch(
() => [props.include, props.exclude],
([include, exclude]) => { // 監(jiān)聽include和exclude,在被修改時(shí)對(duì)cache進(jìn)行修正
include && pruneCache(name => matches(include, name))
exclude && pruneCache(name => !matches(exclude, name))
},
// prune post-render after `current` has been updated
{ flush: 'post', deep: true }
)
??這里的程序邏輯是動(dòng)態(tài)監(jiān)聽include和exclude的改變,從而動(dòng)態(tài)地維護(hù)之前創(chuàng)建的緩存對(duì)象cache,其實(shí)就是對(duì)cache進(jìn)行遍歷,發(fā)現(xiàn)緩存的節(jié)點(diǎn)名稱和新的規(guī)則沒有匹配上時(shí),就把這個(gè)緩存節(jié)點(diǎn)從緩存中摘除。下面來看pruneCache這個(gè)方法,代碼如下:
function pruneCache(filter?: (name: string) => boolean) {
cache.forEach((vnode, key) => {
const name = getComponentName(vnode.type as ConcreteComponent)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
遍歷cache中的所有項(xiàng),如果不符合filter指定的規(guī)則,則會(huì)執(zhí)行pruneCacheEntry,代碼如下:
function pruneCacheEntry(key: CacheKey) {
const cached = cache.get(key) as VNode
if (!current || !isSameVNodeType(cached, current)) {
unmount(cached)
} else if (current) {
// current active instance should no longer be kept-alive.
// we can't unmount it now but it might be later, so reset its flag now.
resetShapeFlag(current)
}
// 銷毀VNode對(duì)應(yīng)的組件實(shí)例
cache.delete(key)
keys.delete(key)
}
上面的內(nèi)容完成以后,當(dāng)響應(yīng)式觸發(fā)時(shí),<keep-alive>中的內(nèi)容會(huì)改變,會(huì)調(diào)用<keep-alive>的render方法得到VNode,這里并沒有用很深層次的diff去對(duì)比緩存前后的VNode,而是直接將舊節(jié)點(diǎn)置為null,用新節(jié)點(diǎn)進(jìn)行替換,在patch方法中,直接命中這里的邏輯,代碼如下:
// n1為緩存前的節(jié)點(diǎn),n2為將要替換的節(jié)點(diǎn)
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
// 卸載舊節(jié)點(diǎn)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
然后通過setup方法中的sharedContext.activate和sharedContext.deactivate來進(jìn)行內(nèi)容的替換,其核心是move方法,代碼如下:
const move: MoveFn = () => {
// 替換DOM
...
hostInsert(el!, container, anchor) // insertBefore修改DOM
}
總結(jié)一下,<keep-alive>組件也是一個(gè)Vue組件,它通過自定義的render方法實(shí)現(xiàn),并且使用了插槽。由于是直接使用VNode方式進(jìn)行內(nèi)容替換,不是直接存儲(chǔ)DOM結(jié)構(gòu),因此不會(huì)執(zhí)行組件內(nèi)的生命周期方法,它通過include和exclude維護(hù)組件的cache對(duì)象,從而來處理緩存中的具體邏輯。