开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第2天,点击查看活动详情
我们先来思考一下,我们需要缓存什么?
组件的递归 patch 过程,主要就是为了渲染 DOM,显然这个递归过程是有一定的性能耗时的,既然目标是为了渲染 DOM,那么我们是不是可以把 DOM 缓存了,这样下一次渲染我们就可以直接从缓存里获取 DOM 并渲染,就不需要每次都重新递归渲染了。
实际上 KeepAlive 组件就是这么做的,它注入了两个钩子函数,onBeforeMount 和 onBeforeUpdate,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存:
const cacheSubtree = () => {
if (pendingCacheKey != null) {
cache.set(pendingCacheKey, instance.subTree)
}
}
由于 pendingCacheKey 是在 KeepAlive 的 render 函数中才会被赋值,所以 KeepAlive 首次进入 onBeforeMount 钩子函数的时候是不会缓存的。
然后 KeepAlive 执行 render 的时候,pendingCacheKey 会被赋值为 vnode.key,我们回过头看一下示例渲染后的模板:
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 的子节点创建的时候都添加了一个 key 的 prop,它就是专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key。
页面首先渲染 A 组件,接着当我们点击按钮的时候,修改了 flag 的值,会触发当前组件的重新渲染,进而也触发了 KeepAlvie 组件的重新渲染,在组件重新渲染前,会执行 onBeforeUpdate 对应的钩子函数,也就再次执行到 cacheSubtree 函数中。
这个时候 pendingCacheKey 对应的是 A 组件 vnode 的 key,instance.subTree 对应的也是 A 组件的渲染子树,所以 KeepAlive 每次在更新前,会缓存前一个组件的渲染子树。
经过前面的分析,我认为 onBeforeMount 的钩子函数注入似乎并没有必要,我在源码中删除后再跑 Vue.js 3.0 的单测也能通过,如果你有不同意见,欢迎在留言区与我分享。
这个时候渲染了 B 组件,当我们再次点击按钮,修改 flag 值的时候,会再次触发KeepAlvie 组件的重新渲染,当然此时执行 onBeforeUpdate 钩子函数缓存的就是 B 组件的渲染子树了。
接着再次执行 KeepAlive 组件的 render 函数,此时就可以从缓存中根据 A 组件的 key 拿到对应的渲染子树 cachedVNode 的了,然后执行如下逻辑:
if (cachedVNode) {
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
// 避免 vnode 节点作为新节点被挂载
vnode.shapeFlag |= 512 /* COMPONENT_KEPT_ALIVE */
// 让这个 key 始终新鲜
keys.delete(key)
keys.add(key)
}
else {
keys.add(key)
// 删除最久不用的 key,符合 LRU 思想
if (max && keys.size > parseInt(max, 10)) {
pruneCacheEntry(keys.values().next().value)
}
}
有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component,赋值给 KeepAlive 的 vnode,并更新 vnode.shapeFlag,以便后续 patch 阶段使用。