基本用法

  1. <keep-alive>
  2. - <div>123</div>
  3. + <component :is="switchOne ? 'TabOne' : 'TabTwo'" />
  4. - <component :is="switchOne ? 'TabTwo' : 'TabOne'" />
  5. </keep-alive>
  • keep-alive 只会去缓存 第一级 第一个 组件
  • 我们可以通过 include / exclude / max 来对缓存进行更小颗粒的控制
  • 缓存的组件在切换的时候会触发 activated / deactivated 的生命周期函数

组件实现原理

  1. export default {
  2. name: 'keep-alive',
  3. abstract: true,
  4. props: {
  5. include: patternTypes,
  6. exclude: patternTypes,
  7. max: [String, Number]
  8. },
  9. created () {
  10. // 创建缓存队列以及其对应的 key 值队列
  11. this.cache = Object.create(null)
  12. this.keys = []
  13. },
  14. destroyed () {
  15. // 销毁所有已经保存的组件实例
  16. for (const key in this.cache) {
  17. pruneCacheEntry(this.cache, key, this.keys)
  18. }
  19. },
  20. mounted () {
  21. // 监听 include / exclued,用于让缓存队列保持一致性
  22. this.$watch('include', val => {
  23. pruneCache(this, name => matches(val, name))
  24. })
  25. this.$watch('exclude', val => {
  26. pruneCache(this, name => !matches(val, name))
  27. })
  28. },
  29. render () {
  30. // 从 $slot 中获取 keep-alive 的 children
  31. const slot = this.$slots.default
  32. // 找到第一个组件的最新的 VNode
  33. const vnode: VNode = getFirstComponentChild(slot)
  34. // 获取其配置信息
  35. const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  36. if (componentOptions) {
  37. // 被捕获的组件名称
  38. const name: ?string = getComponentName(componentOptions)
  39. // 如果不在缓存的目标范围内,则直接返回新的 VNode
  40. const { include, exclude } = this
  41. if (
  42. // not included
  43. (include && (!name || !matches(include, name))) ||
  44. // excluded
  45. (exclude && name && matches(exclude, name))
  46. ) {
  47. return vnode
  48. }
  49. const { cache, keys } = this
  50. // 获得组件对应的 key 值
  51. const key: ?string = vnode.key == null
  52. ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
  53. : vnode.key
  54. // LRU 缓存操作,如果缓存中存在对应的实例,则将实例注入到 VNode 中
  55. if (cache[key]) {
  56. vnode.componentInstance = cache[key].componentInstance
  57. // make current key freshest
  58. remove(keys, key)
  59. keys.push(key)
  60. } else {
  61. cache[key] = vnode
  62. keys.push(key)
  63. // prune oldest entry
  64. if (this.max && keys.length > parseInt(this.max)) {
  65. pruneCacheEntry(cache, keys[0], keys, this._vnode)
  66. }
  67. }
  68. // 标注 keepAlive,方便 Vue 配合
  69. vnode.data.keepAlive = true
  70. }
  71. // 返回
  72. return vnode || (slot && slot[0])
  73. }
  74. }

缓存核心操作的内容其实是 componentInstance,如果已经初始化过对应的实例则不用初始化,直接取出即可。

Vue 内部是如何配合的

先明确一下正常组件实例化的过程:

  • AST(编译 template)
  • CodeGen(生成 render 函数)
  • VNode(通过 render 获得组件 VNode,并将组件的构造函数存到 VNode 中)
  • Patch
    • 新增节点(通过 VNode 中的构造函数初始化组件)
    • 更新节点(基于旧的组件实例更新其状态)
    • 删除节点


如果没有 keep-alive,组件在进行切换到时候,Vue 在 patch 会认为是新增的节点而重新创建组件实例,从而导致先前可能存在状态会被清空。
如果有 keep-alive,切换节点操作最后 patch 还是会走到
新增节点 **的逻辑,但是不一样的是,新的 VNode 中已经有组件实例,同时 VNode.data.keepAlive 标识了是一个 keep-alive 下的组件。

下面是新增组件的函数

  • 这里主要是调用 VNode 的 init()
  • 同时实例化完后会决定是否调用重新激活(activated)生命周期函数

    1. function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    2. let i = vnode.data
    3. if (isDef(i)) {
    4. // 标记是否需要重新激活
    5. const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    6. // 调用 init 钩子函数
    7. if (isDef(i = i.hook) && isDef(i = i.init)) {
    8. i(vnode, false /* hydrating */)
    9. }
    10. // 如果组件实例构建成功
    11. if (isDef(vnode.componentInstance)) {
    12. initComponent(vnode, insertedVnodeQueue)
    13. insert(parentElm, vnode.elm, refElm)
    14. // 如果是重新激活的组件则调用对应的钩子函数
    15. if (isTrue(isReactivated)) {
    16. reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
    17. }
    18. return true
    19. }
    20. }
    21. }

    这里是 keep-alive 与普通组件初始化开始走向分岔路的起点

  • 如果是 keep-alive 下的组件,同时初始化过了,则直接 patch

    1. init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    2. // 如果已经有组件实例 并且 没被销毁 并且有 keepAlive 则走 patch
    3. // 反之走初始化
    4. if (
    5. vnode.componentInstance &&
    6. !vnode.componentInstance._isDestroyed &&
    7. vnode.data.keepAlive
    8. ) {
    9. // kept-alive components, treat as a patch
    10. const mountedNode: any = vnode // work around flow
    11. componentVNodeHooks.prepatch(mountedNode, mountedNode)
    12. } else {
    13. const child = vnode.componentInstance = createComponentInstanceForVnode(
    14. vnode,
    15. activeInstance
    16. )
    17. child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    18. }
    19. },