keep-alive
組件是Vue
提供的組件,它可以緩存組件實(shí)例,在某些情況下避免了組件的掛載和卸載泞歉,在某些場(chǎng)景下非常實(shí)用。
例如最近我們遇到了一種場(chǎng)景匿辩,某個(gè)組件上傳較大的文件是個(gè)耗時(shí)的操作腰耙,如果上傳的時(shí)候切換到其他頁(yè)面內(nèi)容,組件會(huì)被卸載铲球,對(duì)應(yīng)的下載也會(huì)被取消挺庞。此時(shí)可以用keep-alive
組件包裹這個(gè)組件,在切換到其他頁(yè)面時(shí)該組件仍然可以繼續(xù)上傳文件稼病,切換回來(lái)也可以看到上傳進(jìn)度选侨。
keep-alive
渲染子節(jié)點(diǎn)
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
setup(props: KeepAliveProps, { slots }: SetupContext) {
// 需要渲染的子樹(shù)VNode
let current: VNode | null = null
return () => {
// 獲取子節(jié)點(diǎn), 由于Keep-alive只能有一個(gè)子節(jié)點(diǎn),直接取第一個(gè)子節(jié)點(diǎn)
const children = slots.default()
const rawVNode = children[0]
// 標(biāo)記 | ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE然走,這個(gè)組件是`keep-alive`組件, 這個(gè)標(biāo)記 不走 unmount邏輯援制,因?yàn)橐痪彺娴? vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
// 記錄當(dāng)前子節(jié)點(diǎn)
current = vnode
// 返回子節(jié)點(diǎn),代表渲染這個(gè)子節(jié)點(diǎn)
return rawVNode
}
}
}
組件的
setup
返回函數(shù),這個(gè)函數(shù)就是組件的渲染函數(shù);
keep-alive
是一個(gè)虛擬節(jié)點(diǎn)不需要渲染芍瑞,只需要渲染子節(jié)點(diǎn)晨仑,所以函數(shù)只需要返回子節(jié)點(diǎn)VNode就行了。
緩存功能
- 定義存儲(chǔ)緩存數(shù)據(jù)的
Map
, 所有的緩存鍵值數(shù)組Keys
拆檬,代表當(dāng)前子組件的緩存鍵值pendingCacheKey
洪己;
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
- 渲染函數(shù)中獲取子樹(shù)節(jié)點(diǎn)
VNode
的key
, 緩存cache
中查看是否有key
對(duì)應(yīng)的緩存節(jié)點(diǎn)
const key = vnode.key
const cachedVNode = cache.get(key)
key
是生成子節(jié)點(diǎn)的渲染函數(shù)時(shí)添加的,一般情況下就是0竟贯,1答捕,2,...這些數(shù)字屑那。
- 記錄下點(diǎn)前的
key
pendingCacheKey = key
- 如果有找到緩存的
cachedVNode
節(jié)點(diǎn)拱镐,將緩存的cachedVNode
節(jié)點(diǎn)的組件實(shí)例和節(jié)點(diǎn)元素 復(fù)制給新的VNode
節(jié)點(diǎn)。沒(méi)有找到就先將當(dāng)前子樹(shù)節(jié)點(diǎn)VNode
的pendingCacheKey
加入到Keys
中持际。
if (cachedVNode) {
// 復(fù)制節(jié)點(diǎn)
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 標(biāo)記 | ShapeFlags.COMPONENT_KEPT_ALIVE沃琅,這個(gè)組件是復(fù)用的`VNode`, 這個(gè)標(biāo)記 不走 mount邏輯
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
} else {
// 添加 pendingCacheKey
keys.add(key)
}
問(wèn)題: 這里為什么不實(shí)現(xiàn)在
cache
中存入{pendingCacheKey: vnode}
呢?
答案: 這里其實(shí)可以加入這邏輯选酗,只是官方間隔這個(gè)邏輯延后實(shí)現(xiàn)了, 我覺(jué)得沒(méi)什么差別阵难。
- 在組件掛載
onMounted
和更新onUpdated
的時(shí)候添加/更新緩存
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
const cacheSubtree = () => {
if (pendingCacheKey != null) {
// 添加/更新緩存
cache.set(pendingCacheKey, instance.subTree)
}
}
全部代碼
const KeepAliveImpl: ComponentOptions = {
name: `KeepAlive`,
setup(props: KeepAliveProps, { slots }: SetupContext) {
let current: VNode | null = null
// 緩存的一些數(shù)據(jù)
const cache = new Map()
const keys: Keys = new Set()
let pendingCacheKey: CacheKey | null = null
// 更新/添加緩存數(shù)據(jù)
const cacheSubtree = () => {
if (pendingCacheKey != null) {
// 添加/更新緩存
cache.set(pendingCacheKey, instance.subTree)
}
}
// 監(jiān)聽(tīng)生命周期
onMounted(cacheSubtree)
onUpdated(cacheSubtree)
return () => {
const children = slots.default()
const rawVNode = children[0]
// 獲取緩存
const key = rawVNode.key
const cachedVNode = cache.get(key)
pendingCacheKey = key
if (cachedVNode) {
// 復(fù)用DOM和組件實(shí)例
rawVNode.el = cachedVNode.el
rawVNode.component = cachedVNode.component
} else {
// 添加 pendingCacheKey
keys.add(key)
}
rawVNode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
current = rawVNode
return rawVNode
}
}
}
至此岳枷,通過(guò)
cache
實(shí)現(xiàn)了DOM和組件實(shí)例的緩存芒填。
keep-alive
的patch
復(fù)用邏輯
我們知道生成VNode
后是進(jìn)行patch
邏輯呜叫,生成DOM
。
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
}
processComponent
處理組件邏輯的時(shí)候如果是復(fù)用ShapeFlags.COMPONENT_KEPT_ALIVE
則走的父組件keep-alive
的activate
方法殿衰;
const unmount: UnmountFn = (
vnode,
parentComponent,
parentSuspense,
doRemove = false,
optimized = false
) => {
const {
type,
props,
ref,
children,
dynamicChildren,
shapeFlag,
patchFlag,
dirs
} = vnode
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
return
}
}
unmount
卸載的keep-alive
組件ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
時(shí)調(diào)用父組件keep-alive
的deactivate
方法朱庆。
總結(jié):
keep-alive
組件的復(fù)用和卸載被activate
方法和deactivate
方法接管了。
active
邏輯
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
// 1. 直接掛載DOM
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
// 2. 更新prop
patch(
instance.vnode,
vnode,
container,
anchor,
instance,
parentSuspense,
isSVG,
vnode.slotScopeIds,
optimized
)
// 3. 異步執(zhí)行onVnodeMounted 鉤子函數(shù)
queuePostRenderEffect(() => {
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a)
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
}
- 直接掛載DOM
- 更新prop
- 異步執(zhí)行
onVnodeMounted
鉤子函數(shù)
deactivate
邏輯
const storageContainer = createElement('div')
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
// 1. 把DOM移除闷祥,掛載在一個(gè)新建的div下
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
// 2. 異步執(zhí)行onVnodeUnmounted鉤子函數(shù)
queuePostRenderEffect(() => {
if (instance.da) {
invokeArrayFns(instance.da)
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
// Update components tree
devtoolsComponentAdded(instance)
}
}
- 把DOM移除娱颊,掛載在一個(gè)新建的div下
- 異步執(zhí)行
onVnodeUnmounted
鉤子函數(shù)
問(wèn)題:舊節(jié)點(diǎn)的
deactivate
和新節(jié)點(diǎn)的active
誰(shuí)先執(zhí)行
答案:舊節(jié)點(diǎn)的deactivate
先執(zhí)行,新節(jié)點(diǎn)的active
后執(zhí)行凯砍。
keep-alive
的unmount
邏輯
- 將
cache
中出當(dāng)前子樹(shù)VNode節(jié)點(diǎn)外的所有卸載箱硕,當(dāng)前組件取消keep-alive
的標(biāo)記, 這樣當(dāng)前子樹(shù)VNode會(huì)隨著keep-alive
的卸載而卸載。
onBeforeUnmount(() => {
cache.forEach(cached => {
const { subTree, suspense } = instance
const vnode = getInnerChild(subTree)
if (cached.type === vnode.type) {
// 當(dāng)然組件先取消`keep-alive`的標(biāo)記悟衩,能正在執(zhí)行unmout
resetShapeFlag(vnode)
// but invoke its deactivated hook here
const da = vnode.component!.da
da && queuePostRenderEffect(da, suspense)
return
}
// 每個(gè)緩存的VNode剧罩,執(zhí)行unmount方法
unmount(cached)
})
})
<!-- 執(zhí)行unmount -->
function unmount(vnode: VNode) {
// 取消`keep-alive`的標(biāo)記,能正在執(zhí)行unmout
resetShapeFlag(vnode)
// unmout
_unmount(vnode, instance, parentSuspense)
}
keep-alive
卸載了座泳,其緩存的DOM
也將被卸載惠昔。
keep-alive
緩存的配置include
,exclude
和max
這部分知道邏輯就好了,不做代碼分析挑势。
- 組件名稱(chēng)在
include
中的組件會(huì)被緩存;- 組件名稱(chēng)在
exclude
中的組件不會(huì)被緩存;- 規(guī)定緩存的最大數(shù)量镇防,如果超過(guò)了就把緩存的最前面的內(nèi)容刪除。
動(dòng)態(tài)組件
使用方法
<keep-alive>
<component is="A"></component>
</keep-alive>
渲染函數(shù)
resolveDynamicComponent("A")
resolveDynamicComponent
的邏輯
export function resolveDynamicComponent(component: unknown): VNodeTypes {
if (isString(component)) {
return resolveAsset(COMPONENTS, component, false) || component
}
}
function resolveAsset(
type,
name,
warnMissing = true,
maybeSelfReference = false
) {
const res =
// local registration
// check instance[type] first which is resolved for options API
resolve(instance[type] || Component[type], name) ||
// global registration
resolve(instance.appContext[type], name)
return res
}
和指令一樣潮饱,
resolveDynamicComponent
就是根據(jù)名稱(chēng)尋找局部或者全局注冊(cè)的組件来氧,然后渲染對(duì)應(yīng)的組件。