KeepAlive 用来缓存组件实例,让组件在切换时不会被重复创建和销毁。
KeepAlive 用来缓存组件实例,让组件在切换时不会被重复创建和销毁。
常用于动态组件、路由页面切换等场景:组件离开页面时先移动到缓存容器中,再次渲染时直接复用之前的组件实例和真实 DOM。
这样可以保留组件内部状态,比如用户输入、滚动位置等,也可以减少不必要的重新挂载。
<script type="module">
import {
h,
createApp,
KeepAlive,
ref,
onMounted,
onUnmounted,
} from '../dist/vue.esm.js'
const Comp1 = {
name: 'Com1',
setup() {
onMounted(() => {
console.log('Com1 mounted')
})
onUnmounted(() => {
console.log('Com1 unmount')
})
return () => h('div', ['Com1', h('input')])
},
}
const Comp2 = {
name: 'Com2',
setup() {
onMounted(() => {
console.log('Com2 mounted')
})
onUnmounted(() => {
console.log('Com2 unmount')
})
return () => h('div', ['Com2', h('input')])
},
}
const Comp3 = {
name: 'Com3',
setup() {
onMounted(() => {
console.log('Com3 mounted')
})
onUnmounted(() => {
console.log('Com3 unmount')
})
return () => h('div', ['Com3', h('input')])
},
}
const App = {
name: 'App',
setup() {
const components = [Comp1, Comp2, Comp3]
const index = ref(0)
return () => {
return h('div', [
h('button', {
onClick() {
index.value = index.value === 2 ? 0 : index.value + 1
},
}, 'change'),
h(KeepAlive, null, () => h(components[index.value])),
])
}
},
}
createApp(App).mount('#app')
</script>
第一次切换到某个组件时,会正常触发 mounted。
再次切换回来时,不会重新创建组件实例,也不会再次触发 mounted。原来输入框里的内容也会保留下来。
渲染流程
实现主要分为四步:
KeepAlive渲染默认插槽中的组件,并用Map缓存组件vnode- 组件被切走时,不执行真正的卸载,而是移动到隐藏容器中
- 组件再次渲染时,复用缓存中的组件实例和真实
DOM processComponent根据标记决定是挂载新组件,还是激活缓存组件
实现
1. 基础结构
先定义一个最简单的 KeepAlive:
export const isKeepAlive = type => type.__isKeepAlive
export const KeepAlive = {
name: 'KeepAlive',
__isKeepAlive: true,
setup(props, { slots }) {
return () => {
return slots.default()
}
}
}
这样已经可以正常渲染插槽中的组件。
但现在只是把组件原样返回,还没有做缓存。组件切换时,旧组件仍然会走正常的卸载流程。
2. 缓存组件 vnode
export const KeepAlive = {
name: 'KeepAlive',
__isKeepAlive: true,
setup(props, { slots }) {
const cache = new Map()
return () => {
const vnode = slots.default()
const key = vnode.key ?? vnode.type
cache.set(key, vnode)
return vnode
}
}
}
cache 的作用是保存已经渲染过的组件 vnode。
但只缓存 vnode 在组件切换时,旧组件会被 unmount 删除,真实 DOM 和组件实例都会丢失。
所以再给 vnode 打一个标记,告诉渲染器:这个组件需要被 KeepAlive 缓存,不要真的卸载。
return () => {
const vnode = slots.default()
const key = vnode.key ?? vnode.type
cache.set(key, vnode)
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
return vnode
}
COMPONENT_SHOULD_KEEP_ALIVE 表示组件离开页面时应该进入缓存,而不是直接销毁。
3. 暂停卸载
卸载逻辑在渲染器的 unmount 也要识别 COMPONENT_SHOULD_KEEP_ALIVE:
function unmount(vnode) {
const { shapeFlag, children, ref } = vnode
if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
const parentComponent = vnode.component.parent
parentComponent.ctx.deactivate(vnode)
return
}
if (shapeFlag & ShapeFlags.COMPONENT) {
unmountComponent(vnode.component)
}
else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
unmountChildren(children)
}
hostRemove(vnode.el)
if (ref != null) {
setRef(ref, null)
}
}
如果组件需要被缓存,就不再继续执行 unmountComponent 和 hostRemove。
但只是跳过卸载还不够。旧组件的真实 DOM 仍然停留在页面中,所以需要把它移动到一个隐藏容器里。
移动逻辑在 KeepAlive 的 deactivate 中:
export const KeepAlive = {
name: 'KeepAlive',
__isKeepAlive: true,
setup(props, { slots }) {
const cache = new Map()
const instance = getCurrentInstance()
const { options } = instance.ctx.renderer
const { createElement, insert } = options
const storageContainer = createElement('div')
instance.ctx.deactivate = (vnode) => {
insert(vnode.el, storageContainer)
}
return () => {
const vnode = slots.default()
const key = vnode.key ?? vnode.type
cache.set(key, vnode)
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
return vnode
}
}
}
storageContainer 不是挂载到页面上,只是用来临时保存被缓存组件的真实 DOM。
这样组件切走时,组件实例不会销毁,真实 DOM 也只是从当前容器移动到了缓存容器。
4. 传入渲染器能力
KeepAlive 需要调用平台相关的 insert 和 createElement。
这些方法属于 runtime-dom,所以在挂载 KeepAlive 组件时,需要把渲染器能力传给组件实例:
function mountComponent(vnode, container, anchor, parentComponent) {
const instance = createComponentInstance(vnode, parentComponent)
if (isKeepAlive(vnode.type)) {
instance.ctx.renderer = { options }
}
vnode.component = instance
setupComponent(instance)
setupRenderEffect(instance, container, anchor)
}
这样 KeepAlive 内部就可以通过 instance.ctx.renderer.options 拿到平台操作方法。
5. 使用缓存
再次渲染同一个组件时,如果缓存中已经存在对应的 vnode,就复用之前的组件实例和真实 DOM:
export const KeepAlive = {
name: 'KeepAlive',
__isKeepAlive: true,
setup(props, { slots }) {
const cache = new Map()
const instance = getCurrentInstance()
const { options } = instance.ctx.renderer
const { createElement, insert } = options
const storageContainer = createElement('div')
instance.ctx.activate = (vnode, container, anchor) => {
insert(vnode.el, container, anchor)
}
instance.ctx.deactivate = (vnode) => {
insert(vnode.el, storageContainer)
}
return () => {
const vnode = slots.default()
const key = vnode.key ?? vnode.type
const cachedVNode = cache.get(key)
if (cachedVNode) {
vnode.component = cachedVNode.component
vnode.el = cachedVNode.el
vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
}
cache.set(key, vnode)
vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
return vnode
}
}
}
这里有两个标记:
COMPONENT_SHOULD_KEEP_ALIVE:当前组件以后被卸载时,需要进入缓存COMPONENT_KEPT_ALIVE:当前组件已经命中过缓存,本次渲染需要复用
命中缓存时,需要把旧 vnode 上的 component 和 el 复制到新 vnode 上。
这样后续渲染器就能继续使用之前的组件实例和真实 DOM。
6. 激活缓存组件
最后在 processComponent 中处理 COMPONENT_KEPT_ALIVE:
function processComponent(n1, n2, container, anchor, parentComponent) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
parentComponent.ctx.activate(n2, container, anchor)
return
}
if (n1 == null) {
mountComponent(n2, container, anchor, parentComponent)
}
else {
updateComponent(n1, n2)
}
}
如果组件已经被缓存过,就不再调用 mountComponent。
此时只需要调用父组件上的 activate,把缓存容器中的真实 DOM 移回当前容器。