- Contents
KeepAlive 的本质是缓存管理,再加上特殊的挂载/卸载逻辑。
<KeepAlive><CompA v-if="flag"></CompA><CompB v-else></CompB><button @click="flag=!flag">toggle</button></KeepAlive>
模版编译之后的结果:Teleport Explorer
import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode, KeepAlive as _KeepAlive } from "vue"export function render(_ctx, _cache, $props, $setup, $data, $options) {const _component_CompA = _resolveComponent("CompA")const _component_CompB = _resolveComponent("CompB")return (_openBlock(), _createBlock(_KeepAlive, null, [(_ctx.flag)// KeepAlive 的子节点创建的时候都添加了一个 key 的 prop// 专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key? (_openBlock(), _createBlock(_component_CompA, { key: 0 })): (_openBlock(), _createBlock(_component_CompB, { key: 1 })),_createElementVNode("button", {onClick: $event => (_ctx.flag=!_ctx.flag)}, "toggle", 8 /* PROPS */, ["onClick"])], 1024 /* DYNAMIC_SLOTS */))}
KeepAlive 组件对这两个组件做了一层封装,KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM 的流程(而是走激活和失活流程)。
- deactivate 失活:将 KeepAlive 组件从原容器中移动到另一个隐藏容器中,实现「假卸载」
- activate 激活:将该组件从隐藏容器中再搬运到原容器中
缓存管理使用 LRU Cache
组件渲染
函数先从 slots.default() 拿到子节点 children,它就是 KeepAlive 组件包裹的子组件,由于 KeepAlive 只能渲染单个子节点,所以当 children 长度大于 1 的时候会报警告。
先不考虑缓存情况下,KeepAlive 渲染的 vnode 就是子节点 children 的第一个元素,它是函数的返回值。
KeepAlive 是抽象组件,它本身不渲染成实体节点,而是渲染它的第一个子节点。
缓存的设计
KeepAlive 内注入了两个钩子函数,onMounted 和 onUpdated,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存。
const cacheSubtree = () => {// fix #1621, the pendingCacheKey could be 0if (pendingCacheKey != null) {cache.set(pendingCacheKey, getInnerChild(instance.subTree))}}
由于 pendingCacheKey 是在 KeepAlive 的 render 函数中才会被赋值,所以 KeepAlive 首次进入 onMounted 钩子函数的时候是不会缓存的。
然后 KeepAlive 执行 render 的时候,pendingCacheKey 会被赋值为 vnode.key,渲染后的模板。(KeepAlive 的子节点创建的时候都添加了一个 key 的 prop,它就是专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key。请见:上面示例渲染后的模板)
有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component,赋值给 KeepAlive 的 vnode,并更新 vnode.shapeFlag,以便后续 patch 阶段使用。
由于 KeepAlive 缓存的都是有状态的组件 vnode,有缓存和没缓存在 patch 和 unmount 阶段有何区别?
patch
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {n2.slotScopeIds = slotScopeIdsif (n1 == null) {// 处理 KeepAlive 组件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 {// 更新组件}}
KeepAlive 首次渲染某一个子节点时,和正常的组件节点渲染没有区别。
但是有缓存后,由于标记了 shapeFlag,所以在执行 processComponent 函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 activate 激活函数(sharedContext.activate)。
sharedContext.activate 中可以看出由于此时已经能从 vnode.el 中拿到缓存的 DOM 了,所以可以直接调用 move 方法挂载节点(而不是重新挂载),然后执行 patch 方法更新组件,以防止 props 发生变化的情况。接下来,就是通过 queuePostRenderEffect 的方式,在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。
unmount
类似的还有 unmount 阶段
const unmount = (vnode,parentComponent,parentSuspense,doRemove = false,optimized = false) => {const {type,props,ref,children,dynamicChildren,shapeFlag,patchFlag,dirs} = vnode// unset refif (ref != null) {setRef(ref, null, parentSuspense, vnode, true)}// 处理 KeepAlive 组件if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)return}// 省略代码}
有缓存标记 shapeFlag 之后在执行 unmount 函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 deactivate 失活函数(sharedContext.deactivate)。
组件并不会被卸载,而是调用 move 方法挂载到 storageContainer 隐藏容器内,然后执行 queuePostRenderEffect 的方式
缓存管理 - LRU Cache
Vue.js 对于缓存所采用的修剪策略是:LRU Cache,可以通过 max prop 来设置。
<KeepAlive :max="2"><Comp :is="dynamicComp" /></KeepAlive>
源码实现:
const keys: Keys = new Set()if (cachedVNode) {// 有缓存// 省略代码// make this key the freshest// 先删除已有 key,再重新增加,保证最新keys.delete(key)keys.add(key)} else {// 无缓存// 直接新增keys.add(key)// prune oldest entry// 超过 max 限制,则清缓存if (max && keys.size > parseInt(max as string, 10)) {pruneCacheEntry(keys.values().next().value)}}function pruneCacheEntry(key: CacheKey) {const cached = cache.get(key) as VNodeif (!current || cached.type !== current.type) {// 真正的卸载组件unmount(cached)} else if (current) {// current active instance should no longer be kept-alive.// we can't unmount it now but it might be later, so reset its flag now.// 修改 vnode 的 shapeFlag// 不让它再被当作一个 KeepAlive 的 vnode// 这样就可以走正常的卸载逻辑resetShapeFlag(current)}cache.delete(key)keys.delete(key)}
卸载
除 KeepAlive 组件本身的 unmount 之外,当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount 钩子函数
onBeforeUnmount(() => {cache.forEach((cached) => {const { subTree, suspense } = instanceconst vnode = getInnerChild(subTree)if (cached.type === vnode.type) {// current instance will be unmounted as part of keep-alive's unmountresetShapeFlag(vnode)// but invoke its deactivated hook hereconst da = vnode.component!.dada && queuePostRenderEffect(da, suspense)return}unmount(cached)})})
遍历所有缓存的 vnode,并且比对缓存的 vnode 是不是当前 KeepAlive 组件渲染的 vnode
- 是,则执行
resetShapeFlag方法(修改vnode的shapeFlag,不让它再被当作一个 KeepAlive 的 vnode,这样就可以走正常的卸载逻辑)。接着通过queuePostRenderEffect的方式执行子组件的deactivated钩子函数。 - 不是,则执行
unmount方法重置shapeFlag以及执行缓存vnode的整套卸载流程
const KeepAliveImpl: ComponentOptions = {name: `KeepAlive`,// Marker for special handling inside the renderer. We are not using a ===// check directly on KeepAlive in the renderer, because importing it directly// would prevent it from being tree-shaken.__isKeepAlive: true,props: {include: [String, RegExp, Array],exclude: [String, RegExp, Array],max: [String, Number],},setup(props: KeepAliveProps, { slots }: SetupContext) {// 当前 KeepAlive 组件的实例const instance = getCurrentInstance()!// KeepAlive communicates with the instantiated renderer via the// ctx where the renderer passes in its internals,// and the KeepAlive instance exposes activate/deactivate implementations.// The whole point of this is to avoid importing KeepAlive directly in the// renderer to facilitate tree-shaking.// 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入// 该对象会暴露渲染器的一些内部方法,例如其中 move 函数用来将一段 DOM 移动到另一个容器中const sharedContext = instance.ctx as KeepAliveContext// if the internal renderer is not registered, it indicates that this is server-side rendering,// for KeepAlive, we just need to render its childrenif (__SSR__ && !sharedContext.renderer) {return () => {const children = slots.default && slots.default()return children && children.length === 1 ? children[0] : children}}// 创建一个缓存对象// key: vnode.type// value: vnodeconst cache: Cache = new Map()const keys: Keys = new Set()let current: VNode | null = nullif (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {;(instance as any).__v_cache = cache}const parentSuspense = instance.suspenseconst {renderer: {p: patch,m: move,um: _unmount,o: { createElement },},} = sharedContext// 创建隐藏容器const storageContainer = createElement('div')sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {const instance = vnode.component!move(vnode, container, anchor, MoveType.ENTER, parentSuspense)// in case props have changedpatch(instance.vnode,vnode,container,anchor,instance,parentSuspense,isSVG,vnode.slotScopeIds,optimized)queuePostRenderEffect(() => {instance.isDeactivated = falseif (instance.a) {invokeArrayFns(instance.a)}const vnodeHook = vnode.props && vnode.props.onVnodeMountedif (vnodeHook) {invokeVNodeHook(vnodeHook, instance.parent, vnode)}}, parentSuspense)if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {// Update components treedevtoolsComponentAdded(instance)}}sharedContext.deactivate = (vnode: VNode) => {const instance = vnode.component!move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)queuePostRenderEffect(() => {if (instance.da) {invokeArrayFns(instance.da)}const vnodeHook = vnode.props && vnode.props.onVnodeUnmountedif (vnodeHook) {invokeVNodeHook(vnodeHook, instance.parent, vnode)}instance.isDeactivated = true}, parentSuspense)if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {// Update components treedevtoolsComponentAdded(instance)}}function unmount(vnode: VNode) {// reset the shapeFlag so it can be properly unmountedresetShapeFlag(vnode)_unmount(vnode, instance, parentSuspense, true)}function pruneCache(filter?: (name: string) => boolean) {cache.forEach((vnode, key) => {const name = getComponentName(vnode.type as ConcreteComponent)if (name && (!filter || !filter(name))) {pruneCacheEntry(key)}})}function pruneCacheEntry(key: CacheKey) {const cached = cache.get(key) as VNodeif (!current || cached.type !== current.type) {unmount(cached)} else if (current) {// current active instance should no longer be kept-alive.// we can't unmount it now but it might be later, so reset its flag now.resetShapeFlag(current)}cache.delete(key)keys.delete(key)}// prune cache on include/exclude prop changewatch(() => [props.include, props.exclude],([include, exclude]) => {include && pruneCache((name) => matches(include, name))exclude && pruneCache((name) => !matches(exclude, name))},// prune post-render after `current` has been updated{ flush: 'post', deep: true })// cache sub tree after renderlet pendingCacheKey: CacheKey | null = nullconst cacheSubtree = () => {// fix #1621, the pendingCacheKey could be 0if (pendingCacheKey != null) {cache.set(pendingCacheKey, getInnerChild(instance.subTree))}}onMounted(cacheSubtree)onUpdated(cacheSubtree)onBeforeUnmount(() => {cache.forEach((cached) => {const { subTree, suspense } = instanceconst vnode = getInnerChild(subTree)if (cached.type === vnode.type) {// current instance will be unmounted as part of keep-alive's unmountresetShapeFlag(vnode)// but invoke its deactivated hook hereconst da = vnode.component!.dada && queuePostRenderEffect(da, suspense)return}unmount(cached)})})return () => {pendingCacheKey = nullif (!slots.default) {return null}const children = slots.default()const rawVNode = children[0]if (children.length > 1) {if (__DEV__) {warn(`KeepAlive should contain exactly one component child.`)}current = nullreturn children} else if (!isVNode(rawVNode) ||(!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&!(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))) {current = nullreturn rawVNode}let vnode = getInnerChild(rawVNode)const comp = vnode.type as ConcreteComponent// for async components, name check should be based in its loaded// inner component if availableconst name = getComponentName(isAsyncWrapper(vnode)? (vnode.type as ComponentOptions).__asyncResolved || {}: comp)const { include, exclude, max } = propsif ((include && (!name || !matches(include, name))) ||(exclude && name && matches(exclude, name))) {current = vnodereturn rawVNode}const key = vnode.key == null ? comp : vnode.key// 挂载时先获取缓存的组件 vnodeconst cachedVNode = cache.get(key)// clone vnode if it's reused because we are going to mutate itif (vnode.el) {vnode = cloneVNode(vnode)if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {rawVNode.ssContent = vnode}}// #1513 it's possible for the returned vnode to be cloned due to attr// fallthrough or scopeId, so the vnode here may not be the final vnode// that is mounted. Instead of caching it directly, we store the pending// key and cache `instance.subTree` (the normalized vnode) in// beforeMount/beforeUpdate hooks.pendingCacheKey = keyif (cachedVNode) {// 有缓存,则不挂载,而走激活// copy over mounted statevnode.el = cachedVNode.elvnode.component = cachedVNode.componentif (vnode.transition) {// recursively update transition hooks on subTreesetTransitionHooks(vnode, vnode.transition!)}// 避免 vnode 节点作为新节点被挂载// avoid vnode being mounted as freshvnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE// make this key the freshestkeys.delete(key)keys.add(key)} else {keys.add(key)// prune oldest entry// 删除最久不用的 keyif (max && keys.size > parseInt(max as string, 10)) {pruneCacheEntry(keys.values().next().value)}}// avoid vnode being unmounted// 避免 vnode 被卸载vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVEcurrent = vnodereturn isSuspense(rawVNode.type) ? rawVNode : vnode}},}
