• Contents

KeepAlive 的本质是缓存管理,再加上特殊的挂载/卸载逻辑。

  1. <KeepAlive>
  2. <CompA v-if="flag"></CompA>
  3. <CompB v-else></CompB>
  4. <button @click="flag=!flag">toggle</button>
  5. </KeepAlive>

模版编译之后的结果:Teleport Explorer

  1. import { resolveComponent as _resolveComponent, openBlock as _openBlock, createBlock as _createBlock, createCommentVNode as _createCommentVNode, createElementVNode as _createElementVNode, KeepAlive as _KeepAlive } from "vue"
  2. export function render(_ctx, _cache, $props, $setup, $data, $options) {
  3. const _component_CompA = _resolveComponent("CompA")
  4. const _component_CompB = _resolveComponent("CompB")
  5. return (_openBlock(), _createBlock(_KeepAlive, null, [
  6. (_ctx.flag)
  7. // KeepAlive 的子节点创建的时候都添加了一个 key 的 prop
  8. // 专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key
  9. ? (_openBlock(), _createBlock(_component_CompA, { key: 0 }))
  10. : (_openBlock(), _createBlock(_component_CompB, { key: 1 })),
  11. _createElementVNode("button", {
  12. onClick: $event => (_ctx.flag=!_ctx.flag)
  13. }, "toggle", 8 /* PROPS */, ["onClick"])
  14. ], 1024 /* DYNAMIC_SLOTS */))
  15. }

KeepAlive 组件对这两个组件做了一层封装,KeepAlive 是一个抽象组件,它并不会渲染成一个真实的 DOM,只会渲染内部包裹的子节点,并且让内部的子组件在切换的时候,不会走一整套递归卸载和挂载 DOM 的流程(而是走激活和失活流程)。

  • deactivate 失活:将 KeepAlive 组件从原容器中移动到另一个隐藏容器中,实现「假卸载」
  • activate 激活:将该组件从隐藏容器中再搬运到原容器中

缓存管理使用 LRU Cache

组件渲染

函数先从 slots.default() 拿到子节点 children,它就是 KeepAlive 组件包裹的子组件,由于 KeepAlive 只能渲染单个子节点,所以当 children 长度大于 1 的时候会报警告。

先不考虑缓存情况下,KeepAlive 渲染的 vnode 就是子节点 children 的第一个元素,它是函数的返回值。

KeepAlive 是抽象组件,它本身不渲染成实体节点,而是渲染它的第一个子节点。

缓存的设计

KeepAlive 内注入了两个钩子函数,onMountedonUpdated,在这两个钩子函数内部都执行了 cacheSubtree 函数来做缓存。

  1. const cacheSubtree = () => {
  2. // fix #1621, the pendingCacheKey could be 0
  3. if (pendingCacheKey != null) {
  4. cache.set(pendingCacheKey, getInnerChild(instance.subTree))
  5. }
  6. }

由于 pendingCacheKey 是在 KeepAlive 的 render 函数中才会被赋值,所以 KeepAlive 首次进入 onMounted 钩子函数的时候是不会缓存的。

然后 KeepAlive 执行 render 的时候,pendingCacheKey 会被赋值为 vnode.key,渲染后的模板。(KeepAlive 的子节点创建的时候都添加了一个 keyprop,它就是专门为 KeepAlive 的缓存设计的,这样每一个子节点都能有一个唯一的 key。请见:上面示例渲染后的模板)

有了缓存的渲染子树后,我们就可以直接拿到它对应的 DOM 以及组件实例 component,赋值给 KeepAlive 的 vnode,并更新 vnode.shapeFlag,以便后续 patch 阶段使用。

由于 KeepAlive 缓存的都是有状态的组件 vnode,有缓存和没缓存在 patchunmount 阶段有何区别?

patch

  1. const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
  2. n2.slotScopeIds = slotScopeIds
  3. if (n1 == null) {
  4. // 处理 KeepAlive 组件
  5. if (n2.shapeFlag & 512 /* COMPONENT_KEPT_ALIVE */) {
  6. parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized)
  7. }
  8. else {
  9. // 挂载组件
  10. mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
  11. }
  12. }
  13. else {
  14. // 更新组件
  15. }
  16. }

KeepAlive 首次渲染某一个子节点时,和正常的组件节点渲染没有区别。

但是有缓存后,由于标记了 shapeFlag,所以在执行 processComponent 函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 activate 激活函数(sharedContext.activate)。

sharedContext.activate 中可以看出由于此时已经能从 vnode.el 中拿到缓存的 DOM 了,所以可以直接调用 move 方法挂载节点(而不是重新挂载),然后执行 patch 方法更新组件,以防止 props 发生变化的情况。接下来,就是通过 queuePostRenderEffect 的方式,在组件渲染完毕后,执行子节点组件定义的 activated 钩子函数。

unmount

类似的还有 unmount 阶段

  1. const unmount = (
  2. vnode,
  3. parentComponent,
  4. parentSuspense,
  5. doRemove = false,
  6. optimized = false
  7. ) => {
  8. const {
  9. type,
  10. props,
  11. ref,
  12. children,
  13. dynamicChildren,
  14. shapeFlag,
  15. patchFlag,
  16. dirs
  17. } = vnode
  18. // unset ref
  19. if (ref != null) {
  20. setRef(ref, null, parentSuspense, vnode, true)
  21. }
  22. // 处理 KeepAlive 组件
  23. if (shapeFlag & ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE) {
  24. ;(parentComponent!.ctx as KeepAliveContext).deactivate(vnode)
  25. return
  26. }
  27. // 省略代码
  28. }

有缓存标记 shapeFlag 之后在执行 unmount 函数时会走到处理 KeepAlive 组件的逻辑中,执行 KeepAlive 组件实例上下文中的 deactivate 失活函数(sharedContext.deactivate)。

组件并不会被卸载,而是调用 move 方法挂载到 storageContainer 隐藏容器内,然后执行 queuePostRenderEffect 的方式

缓存管理 - LRU Cache

Vue.js 对于缓存所采用的修剪策略是:LRU Cache,可以通过 max prop 来设置。

  1. <KeepAlive :max="2">
  2. <Comp :is="dynamicComp" />
  3. </KeepAlive>

源码实现:

  1. const keys: Keys = new Set()
  2. if (cachedVNode) {
  3. // 有缓存
  4. // 省略代码
  5. // make this key the freshest
  6. // 先删除已有 key,再重新增加,保证最新
  7. keys.delete(key)
  8. keys.add(key)
  9. } else {
  10. // 无缓存
  11. // 直接新增
  12. keys.add(key)
  13. // prune oldest entry
  14. // 超过 max 限制,则清缓存
  15. if (max && keys.size > parseInt(max as string, 10)) {
  16. pruneCacheEntry(keys.values().next().value)
  17. }
  18. }
  19. function pruneCacheEntry(key: CacheKey) {
  20. const cached = cache.get(key) as VNode
  21. if (!current || cached.type !== current.type) {
  22. // 真正的卸载组件
  23. unmount(cached)
  24. } else if (current) {
  25. // current active instance should no longer be kept-alive.
  26. // we can't unmount it now but it might be later, so reset its flag now.
  27. // 修改 vnode 的 shapeFlag
  28. // 不让它再被当作一个 KeepAlive 的 vnode
  29. // 这样就可以走正常的卸载逻辑
  30. resetShapeFlag(current)
  31. }
  32. cache.delete(key)
  33. keys.delete(key)
  34. }

卸载

除 KeepAlive 组件本身的 unmount 之外,当 KeepAlive 所在的组件卸载时,由于卸载的递归特性,也会触发 KeepAlive 组件的卸载,在卸载的过程中会执行 onBeforeUnmount 钩子函数

  1. onBeforeUnmount(() => {
  2. cache.forEach((cached) => {
  3. const { subTree, suspense } = instance
  4. const vnode = getInnerChild(subTree)
  5. if (cached.type === vnode.type) {
  6. // current instance will be unmounted as part of keep-alive's unmount
  7. resetShapeFlag(vnode)
  8. // but invoke its deactivated hook here
  9. const da = vnode.component!.da
  10. da && queuePostRenderEffect(da, suspense)
  11. return
  12. }
  13. unmount(cached)
  14. })
  15. })

遍历所有缓存的 vnode,并且比对缓存的 vnode 是不是当前 KeepAlive 组件渲染的 vnode

  • 是,则执行 resetShapeFlag 方法(修改 vnodeshapeFlag,不让它再被当作一个 KeepAlive 的 vnode,这样就可以走正常的卸载逻辑)。接着通过 queuePostRenderEffect 的方式执行子组件的 deactivated 钩子函数。
  • 不是,则执行 unmount 方法重置 shapeFlag 以及执行缓存 vnode 的整套卸载流程
  1. const KeepAliveImpl: ComponentOptions = {
  2. name: `KeepAlive`,
  3. // Marker for special handling inside the renderer. We are not using a ===
  4. // check directly on KeepAlive in the renderer, because importing it directly
  5. // would prevent it from being tree-shaken.
  6. __isKeepAlive: true,
  7. props: {
  8. include: [String, RegExp, Array],
  9. exclude: [String, RegExp, Array],
  10. max: [String, Number],
  11. },
  12. setup(props: KeepAliveProps, { slots }: SetupContext) {
  13. // 当前 KeepAlive 组件的实例
  14. const instance = getCurrentInstance()!
  15. // KeepAlive communicates with the instantiated renderer via the
  16. // ctx where the renderer passes in its internals,
  17. // and the KeepAlive instance exposes activate/deactivate implementations.
  18. // The whole point of this is to avoid importing KeepAlive directly in the
  19. // renderer to facilitate tree-shaking.
  20. // 对于 KeepAlive 组件来说,它的实例上存在特殊的 keepAliveCtx 对象,该对象由渲染器注入
  21. // 该对象会暴露渲染器的一些内部方法,例如其中 move 函数用来将一段 DOM 移动到另一个容器中
  22. const sharedContext = instance.ctx as KeepAliveContext
  23. // if the internal renderer is not registered, it indicates that this is server-side rendering,
  24. // for KeepAlive, we just need to render its children
  25. if (__SSR__ && !sharedContext.renderer) {
  26. return () => {
  27. const children = slots.default && slots.default()
  28. return children && children.length === 1 ? children[0] : children
  29. }
  30. }
  31. // 创建一个缓存对象
  32. // key: vnode.type
  33. // value: vnode
  34. const cache: Cache = new Map()
  35. const keys: Keys = new Set()
  36. let current: VNode | null = null
  37. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  38. ;(instance as any).__v_cache = cache
  39. }
  40. const parentSuspense = instance.suspense
  41. const {
  42. renderer: {
  43. p: patch,
  44. m: move,
  45. um: _unmount,
  46. o: { createElement },
  47. },
  48. } = sharedContext
  49. // 创建隐藏容器
  50. const storageContainer = createElement('div')
  51. sharedContext.activate = (vnode, container, anchor, isSVG, optimized) => {
  52. const instance = vnode.component!
  53. move(vnode, container, anchor, MoveType.ENTER, parentSuspense)
  54. // in case props have changed
  55. patch(
  56. instance.vnode,
  57. vnode,
  58. container,
  59. anchor,
  60. instance,
  61. parentSuspense,
  62. isSVG,
  63. vnode.slotScopeIds,
  64. optimized
  65. )
  66. queuePostRenderEffect(() => {
  67. instance.isDeactivated = false
  68. if (instance.a) {
  69. invokeArrayFns(instance.a)
  70. }
  71. const vnodeHook = vnode.props && vnode.props.onVnodeMounted
  72. if (vnodeHook) {
  73. invokeVNodeHook(vnodeHook, instance.parent, vnode)
  74. }
  75. }, parentSuspense)
  76. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  77. // Update components tree
  78. devtoolsComponentAdded(instance)
  79. }
  80. }
  81. sharedContext.deactivate = (vnode: VNode) => {
  82. const instance = vnode.component!
  83. move(vnode, storageContainer, null, MoveType.LEAVE, parentSuspense)
  84. queuePostRenderEffect(() => {
  85. if (instance.da) {
  86. invokeArrayFns(instance.da)
  87. }
  88. const vnodeHook = vnode.props && vnode.props.onVnodeUnmounted
  89. if (vnodeHook) {
  90. invokeVNodeHook(vnodeHook, instance.parent, vnode)
  91. }
  92. instance.isDeactivated = true
  93. }, parentSuspense)
  94. if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
  95. // Update components tree
  96. devtoolsComponentAdded(instance)
  97. }
  98. }
  99. function unmount(vnode: VNode) {
  100. // reset the shapeFlag so it can be properly unmounted
  101. resetShapeFlag(vnode)
  102. _unmount(vnode, instance, parentSuspense, true)
  103. }
  104. function pruneCache(filter?: (name: string) => boolean) {
  105. cache.forEach((vnode, key) => {
  106. const name = getComponentName(vnode.type as ConcreteComponent)
  107. if (name && (!filter || !filter(name))) {
  108. pruneCacheEntry(key)
  109. }
  110. })
  111. }
  112. function pruneCacheEntry(key: CacheKey) {
  113. const cached = cache.get(key) as VNode
  114. if (!current || cached.type !== current.type) {
  115. unmount(cached)
  116. } else if (current) {
  117. // current active instance should no longer be kept-alive.
  118. // we can't unmount it now but it might be later, so reset its flag now.
  119. resetShapeFlag(current)
  120. }
  121. cache.delete(key)
  122. keys.delete(key)
  123. }
  124. // prune cache on include/exclude prop change
  125. watch(
  126. () => [props.include, props.exclude],
  127. ([include, exclude]) => {
  128. include && pruneCache((name) => matches(include, name))
  129. exclude && pruneCache((name) => !matches(exclude, name))
  130. },
  131. // prune post-render after `current` has been updated
  132. { flush: 'post', deep: true }
  133. )
  134. // cache sub tree after render
  135. let pendingCacheKey: CacheKey | null = null
  136. const cacheSubtree = () => {
  137. // fix #1621, the pendingCacheKey could be 0
  138. if (pendingCacheKey != null) {
  139. cache.set(pendingCacheKey, getInnerChild(instance.subTree))
  140. }
  141. }
  142. onMounted(cacheSubtree)
  143. onUpdated(cacheSubtree)
  144. onBeforeUnmount(() => {
  145. cache.forEach((cached) => {
  146. const { subTree, suspense } = instance
  147. const vnode = getInnerChild(subTree)
  148. if (cached.type === vnode.type) {
  149. // current instance will be unmounted as part of keep-alive's unmount
  150. resetShapeFlag(vnode)
  151. // but invoke its deactivated hook here
  152. const da = vnode.component!.da
  153. da && queuePostRenderEffect(da, suspense)
  154. return
  155. }
  156. unmount(cached)
  157. })
  158. })
  159. return () => {
  160. pendingCacheKey = null
  161. if (!slots.default) {
  162. return null
  163. }
  164. const children = slots.default()
  165. const rawVNode = children[0]
  166. if (children.length > 1) {
  167. if (__DEV__) {
  168. warn(`KeepAlive should contain exactly one component child.`)
  169. }
  170. current = null
  171. return children
  172. } else if (
  173. !isVNode(rawVNode) ||
  174. (!(rawVNode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) &&
  175. !(rawVNode.shapeFlag & ShapeFlags.SUSPENSE))
  176. ) {
  177. current = null
  178. return rawVNode
  179. }
  180. let vnode = getInnerChild(rawVNode)
  181. const comp = vnode.type as ConcreteComponent
  182. // for async components, name check should be based in its loaded
  183. // inner component if available
  184. const name = getComponentName(
  185. isAsyncWrapper(vnode)
  186. ? (vnode.type as ComponentOptions).__asyncResolved || {}
  187. : comp
  188. )
  189. const { include, exclude, max } = props
  190. if (
  191. (include && (!name || !matches(include, name))) ||
  192. (exclude && name && matches(exclude, name))
  193. ) {
  194. current = vnode
  195. return rawVNode
  196. }
  197. const key = vnode.key == null ? comp : vnode.key
  198. // 挂载时先获取缓存的组件 vnode
  199. const cachedVNode = cache.get(key)
  200. // clone vnode if it's reused because we are going to mutate it
  201. if (vnode.el) {
  202. vnode = cloneVNode(vnode)
  203. if (rawVNode.shapeFlag & ShapeFlags.SUSPENSE) {
  204. rawVNode.ssContent = vnode
  205. }
  206. }
  207. // #1513 it's possible for the returned vnode to be cloned due to attr
  208. // fallthrough or scopeId, so the vnode here may not be the final vnode
  209. // that is mounted. Instead of caching it directly, we store the pending
  210. // key and cache `instance.subTree` (the normalized vnode) in
  211. // beforeMount/beforeUpdate hooks.
  212. pendingCacheKey = key
  213. if (cachedVNode) {
  214. // 有缓存,则不挂载,而走激活
  215. // copy over mounted state
  216. vnode.el = cachedVNode.el
  217. vnode.component = cachedVNode.component
  218. if (vnode.transition) {
  219. // recursively update transition hooks on subTree
  220. setTransitionHooks(vnode, vnode.transition!)
  221. }
  222. // 避免 vnode 节点作为新节点被挂载
  223. // avoid vnode being mounted as fresh
  224. vnode.shapeFlag |= ShapeFlags.COMPONENT_KEPT_ALIVE
  225. // make this key the freshest
  226. keys.delete(key)
  227. keys.add(key)
  228. } else {
  229. keys.add(key)
  230. // prune oldest entry
  231. // 删除最久不用的 key
  232. if (max && keys.size > parseInt(max as string, 10)) {
  233. pruneCacheEntry(keys.values().next().value)
  234. }
  235. }
  236. // avoid vnode being unmounted
  237. // 避免 vnode 被卸载
  238. vnode.shapeFlag |= ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE
  239. current = vnode
  240. return isSuspense(rawVNode.type) ? rawVNode : vnode
  241. }
  242. },
  243. }