代码运行结果
代码示例
const c1 = {
name: "c1",
setup() {
return () => {
console.log('c1 渲染')
return h('div', null, 'c1')
}
}
}
const c2 = {
name: "c2",
setup() {
return () => {
console.log('c2 渲染')
return h('div', null, 'c2')
}
}
}
render(h(KeepAlive, null, { default: () => h(c1) }), app)
setTimeout(() => {
render(h(KeepAlive, null, { default: () => h(c2) }), app)
}, 2000)
setTimeout(() => {
render(h(KeepAlive, null, { default: () => h(c1) }), app)
}, 3000)
第一:第一次 KeepAlive 组件挂载的时候,还是走正常逻辑,组件挂载可以看之前的这篇文章, # [Vue 源码] Vue 3.2 - 组件挂载原理 这里我们直接到执行 setup 函数,及其返回的 render 函数流程。
- 执行 setup 函数。
-
const keys: Keys = new Set()存储缓存组件的 key, 如果没有就存储 组件的 name。 -
const cache: Cache = new Map()存储缓存 KeepAlive 组件 name 和 subTree 的映射 (也就是组件 rener函数/vue 模板的虚拟dom) -
onMounted(cacheSubtree)挂载的时候去收集 subTree 和 Name 映射。本案例是 c1 -> c1 的subTree. -
onUpdated(cacheSubtree)更新的时候去收集 subTree 和 Name 映射。 -
初始化 activate 和 deactivate 方法,前者是 激活 KeepAlive 组件方法,后者是 KeepAlive 卸载的失活方法。
-
从组件示例的 ctx 拿到 renderer 渲染器,上面定义了一些供 keepAlive 操作 Dom 的一些方法。这些方法是在第一次挂载 keepAlive 组件时挂载的。
-
// mountComponent 这些方法是在第一次挂载 keepAlive 组件时挂载的。
// inject renderer internals for keepAlive
if (isKeepAlive(initialVNode)) {
;(instance.ctx as KeepAliveContext).renderer = internals
}
// 定义了一些供 keepAlive 操作 Dom 的一些方法
const {
renderer: {
p: patch,
m: move,
um: _unmount,
o: { createElement }
}
} = sharedContext
- 执行 setup 函数返回的 render 函数。
- keepAlive 组件的 children 会被编译成组件的 default 插槽。
render(h(KeepAlive, null, { default: () => h(c1) }), app) - 渲染的时候通过 const children = slots.default(),取出默认插槽进行渲染。
- keepAlive 组件的 children 会被编译成组件的 default 插槽。
至此第一次挂载完毕,页面上显示出了 c1.并在控制台打印出 c1 渲染。
- 两秒过后,再次渲染 c2 的 KeepAlive 组件,先执行 c1 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
sharedContext.deactivate = (vnode: VNode) => {
const instance = vnode.component!
move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
}
- 在 deactivate 中将 c1 的 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。
- storageContainer 就是在 KeepAlive 中创建的 div,
const storageContainer = createElement('div'). - c1 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
- 开始渲染 c2 KeepAlive 组件, setup 函数 -> render 函数, 再将 c2 和 对应的subTree 放入缓存中去
- 执行 c2 的 render 函数,打印 c2 渲染,返回虚拟dom,patch 更新插槽之后完成渲染。
自此 c2 被渲染到了页面上。
- 过了一秒后,再次渲染 c1 的 KeepAlive 组件,先执行 c2 的 unmout 方法,keepAlive 组件 unmount 的时 候,会调用 deactivate 方法.
- 在 deactivate 中将 c2 的 dom 并没有被卸载/remoe, 而是还存在了 storageContainer 这个 dom 中。
- storageContainer 就是在 KeepAlive 中创建的 div,
const storageContainer = createElement('div'). - c2 从页面上消失,因为已经把 subTree 移到了 storageContainer 中。
- 开始渲染 c1 KeepAlive 组件,发现 c1 已经被缓存过了, 给 KeepAlive innerChild 孩子打上COMPONENT_KEPT_ALIVE 标记 。
if (cachedVNode) {
// copy over mounted state
vnode.el = cachedVNode.el
vnode.component = cachedVNode.component
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
}
- 由于 KeepALive 组件的第一个孩子 vnode 的 shapeFlag 打上了缓存标记,所以走 active 逻辑。
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
isSVG,
optimized
)
}
- 在 active 中将 subTree 指向的 dom 引用,从 storageContainer 缓存容器中移动了回来。并没有触发 render 函数,这时候界面上已经 有 c1 了。
sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
const instance = vnode.component!
move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
自此 KeepAlive 缓存组件渲染完毕!
核心原理
KeepAlive 中的核心原理,就是 dom 并没有被卸载/remoe, 而是缓存在了 storageContainer 这个 dom 中。希望以下的这段代码可以给读者更多的启示和思考。
当然 KeepAlive 中的 LRU 最近最少使用算法,也值得读者去阅读和探索。
<div id="app"></div>
<div id="storage"></div>
<script>
let divItem = document.createElement('div')
divItem.innerHTML = 'app'
let obj = { item: divItem }
storage.appendChild(obj.item)
setTimeout(() => {
app.appendChild(obj.item)
}, 2000)
</script>