<keep-alive>
<comp-a v-if="flag"></comp-a>
<comp-b v-else></comp-b>
<button @click="flag=!flag">toggle</button>
</keep-alive>
编译之后的render函数
import { resolveComponent as _resolveComponent, createVNode as _createVNode, createCommentVNode as _createCommentVNode, KeepAlive as _KeepAlive, openBlock as _openBlock, createBlock as _createBlock } from "vue"
export function render(_ctx, _cache, $props, $setup, $data, $options) {
const _component_comp_a = _resolveComponent("comp-a")
const _component_comp_b = _resolveComponent("comp-b")
return (_openBlock(), _createBlock(_KeepAlive, null, [
(_ctx.flag)
? _createVNode(_component_comp_a, { key: 0 })
: _createVNode(_component_comp_b, { key: 1 }),
_createVNode("button", {
onClick: $event => (_ctx.flag=!_ctx.flag)
}, "toggle", 8 /* PROPS */, ["onClick"])
], 1024 /* DYNAMIC_SLOTS */))
}
可见使用了KeepAlive组件对组件做了封装,他是一个抽象组件,并不会渲染成一个真实的DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载DOM的流程。
KeepAlive的实现分成四部分:组件的渲染、缓存的设计、props设计、组件的卸载。
KeepAlive 的缓存设计,KeepAlive 包裹的子组件在其渲染后,下一次 KeepAlive 组件更新前会被缓存,缓存后的子组件在下一次渲染的时候直接从缓存中拿到子树 vnode 以及对应的 DOM 元素,直接渲染即可。
props设计:
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
组价卸载:
切换按钮引起的keepalive内部组件的卸载,keepalive所在的组件卸载导致的 keepalive组件整个被卸载
const KeepAliveImpl = {
name: `KeepAlive`,
__isKeepAlive: true,
inheritRef: true,
props: {
include: [String, RegExp, Array],
exclude: [String, RegExp, Array],
max: [String, Number]
},
setup(props, { slots }) {
const cache = new Map()
const keys = new Set()
let current = null
const instance = getCurrentInstance()
const parentSuspense = instance.suspense
const sharedContext = instance.ctx
const { renderer: { p: patch, m: move, um: _unmount, o: { createElement } } } = sharedContext
const storageContainer = createElement('div')
//当从A切换到B时,B组件activate
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component
move(vnode, container, anchor, 0 /* ENTER */, parentSuspense)
patch(instance.vnode, vnode, container, anchor, instance, parentSuspense, isSVG, optimized)
queuePostRenderEffect(() => { //在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。
instance.isDeactivated = false
if (instance.a) {
invokeArrayFns(instance.a)
}
const vnodeHook = vnode.props && vnode.props.onVnodeMounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
}, parentSuspense)
}
//当从A切换到B时,A组件被卸载
sharedContext.deactivate = (vnode) => {
const instance = vnode.component
//只是移出了DOM,没哟真正意义上执行子组件的整套卸载流程
move(vnode, storageContainer, null, 1 /* LEAVE */, parentSuspense)
queuePostRenderEffect(() => { //执行自定义的deactivated 钩子函数
if (instance.da) {
invokeArrayFns(instance.da)
}
const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
if (vnodeHook) {
invokeVNodeHook(vnodeHook, instance.parent, vnode)
}
instance.isDeactivated = true
}, parentSuspense)
}
function unmount(vnode) {
resetShapeFlag(vnode)
_unmount(vnode, instance, parentSuspense)
}
function pruneCache(filter) {
cache.forEach((vnode, key) => {
const name = getName(vnode.type)
if (name && (!filter || !filter(name))) {
pruneCacheEntry(key)
}
})
}
function pruneCacheEntry(key) {
const cached = cache.get(key)
if (!current || cached.type !== current.type) {
unmount(cached)
}
else if (current) {
resetShapeFlag(current)
}
cache.delete(key)
keys.delete(key)
}
//监听props的变化。
//当 include 发生变化的时候,从缓存中删除那些 name 不匹配 include 的 vnode 节点;
//当 exclude 发生变化的时候,从缓存中删除那些 name 匹配 exclude 的 vnode 节点。
watch(() => [props.include, props.exclude], ([include, exclude]) => {
include && pruneCache(name => matches(include, name))
exclude && !pruneCache(name => matches(exclude, name))
})
let pendingCacheKey = null
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, instance.subTree)
}
}
//缓存的设计:初次渲染的时候会赋值key给pendingCacheKey
onBeforeMount(cacheSubtree)
onBeforeUpdate(cacheSubtree) //beforeUpdated的时候把instance.subTree和key存起来
onBeforeUnmount(() => { //keepAlive所在的组件卸载。keepAlive也会被卸载
cache.forEach(cached => {//遍历所有缓存的vnode
const { subTree, suspense } = instance
if (cached.type === subTree.type) {//缓存的vnode是不是当前keepalive组件渲染的vnode
resetShapeFlag(subTree) //修改shapeFla,不在被当做一个keepalive的vnode了,就可以走正常的卸载流程了
const da = subTree.component.da
da && queuePostRenderEffect(da, suspense) //通过 queuePostRenderEffect 的方式执行子组件的 deactivated 钩子函数。
return
}
unmount(cached) //重置 shapeFlag 以及执行缓存 vnode 的整套卸载流程
})
})
//当setup函数返回的是一个函数,那么这个函数就是组件的渲染函数。
return () => {
pendingCacheKey = null
if (!slots.default) {
return null
}
const children = slots.default()
let vnode = children[0]
if (children.length > 1) {//keepalive只能渲染单个子节点。
if ((process.env.NODE_ENV !== 'production')) {
warn(`KeepAlive should contain exactly one component child.`)
}
current = null
return children
}
else if (!isVNode(vnode) ||
!(vnode.shapeFlag & 4 /* STATEFUL_COMPONENT */)) {
current = null
return vnode
}
const comp = vnode.type
const name = getName(comp)
const { include, exclude, max } = props
if ((include && (!name || !matches(include, name))) ||
(exclude && name && matches(exclude, name))) {
return (current = vnode) //props条件不匹配的直接返回节点
}
const key = vnode.key == null ? comp : vnode.key
const cachedVNode = cache.get(key)
if (vnode.el) {
vnode = cloneVNode(vnode)
}
pendingCacheKey = key //初次渲染的时候会赋值key,编译出来的render函数可以看到会加个key
//当再次切回来的时候可以拿到之后缓存的节点
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */ //避免vnode节点作为新节点被挂载
keys.delete(key) //保证key的新鲜
keys.add(key)
}
else { //添加key.如果有最大值或者超过10个,末尾添加首位移出。按照最近最久未使用的LRU算法
keys.add(key)
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
vnode.shapeFlag |= 256 /* COMPONENT_SHOULD_KEEP_ALIVE */
current = vnode
return vnode
}
}
}
有缓存和没有缓存在patch阶段有什么区别?
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
if (n1 == null) {
// 处理 KeepAlive 组件
//有缓存之后被标记shapeFlag,所以会走下边的逻辑。会调用keepalive中定义的sharedContext.activate
if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
}
else {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
}
else {
// 更新组件
}
}
当 flag 为 true 的时候,渲染 A 组件,然后我们点击按钮修改 flag 的值,会触发 KeepAlive 组件的重新渲染,会先执行 BeforeUpdate 钩子函数缓存 A 组件对应的渲染子树 vnode,然后再执行 patch 更新子组件。
这个时候会执行 B 组件的渲染,以及 A 组件的卸载,我们知道组件的卸载会执行 unmount 方法
const unmount = (vnode, parentComponent, parentSuspense, doRemove = false) => {
const { shapeFlag } = vnode
if (shapeFlag & 256 /* COMPONENT_SHOULD_KEEP_ALIVE */) {
parentComponent.ctx.deactivate(vnode)
return
}
// 卸载组件
}
总结: 1,KeepAlive是一个内置组件,只渲染内部节点,且children只能有1个 2,初始化渲染的时候,会经过一些判断,如果children个数大于1,或者不是vnode,或者不在include再exclude的,直接返回子节点。 没有缓存添加key到keys,并且判断max,做新鲜化处理。 最后设置shapeFlag,返回vnode 3,当有切换的时候会触发更新,A切到B,触发beforeUpdate钩子函数,此时会通过cache保存key和instance.subtree. A组件unmount,deactivate,B组件activate,然后patch更新子组件