Vue源码(十一)keep-alive 原理

1,954 阅读6分钟

前言

通过这篇文章可以了解如下内容

  • keep-alive原理

<keep-alive>在Vue 源码中是一个内置组件,它的定义在 src/core/components/keep-alive.js 中:

export default {
  name: 'keep-alive',
  abstract: true,
  props: {
    include: patternTypes,
    exclude: patternTypes,
    max: [String, Number]
  },
  created () {},
  destroyed () {},
  mounted () {},
  render () {}
}

Vue导出了一个对象,也就是说keep-alive组件的内容有abstract属性(后面说这个属性的作用)、createdmounteddestroyed生命周期函数、自定义render函数,并接收3个属性includeexcludemax分别是需要缓存的组件列表,不需要缓存的组件列表和缓存列表最大长度(后面说)

接下来从下面这个demo的整个流程说起

import Vue from 'vue'
// 组件A
const A = {
  name: 'A',
  template: `<div class="a"> 这是 A </div>`,
  created () {
    console.log('A 执行了 created')
  },
  mounted () {
    console.log('A 执行了 mounted')
  },
  activated () {
    console.log('A 执行了 activated')
  },
  deactivated () {
    console.log('A 执行了 deactivated')
  }
}
// 组件B
const B = {
  name: 'B',
  template: `<div class="b"> 这是 B </div>`,
  created () {
    console.log('B 执行了 created')
  },
  mounted () {
    console.log('B 执行了 mounted')
  },
  activated () {
    console.log('B 执行了 activated')
  },
  deactivated () {
    console.log('B 执行了 deactivated')
  }
}
new Vue({
  components: { A, B },
  el: '#app',
  template: `<div>
  <keep-alive>
    <component :is="com"></component>
  </keep-alive>
  <button @click="change">点击</button> // 点击切换组件
  </div>`,
  data () {
    return {
      com: 'A'
    }
  },
  methods: {
    change () {
      this.com = this.com === 'A' ? 'B' : 'A'
    }
  }
})

定义了两个组件AB,分别定义了 4 个生命周期。根组件引用这俩组件,并包在<keep-alive>组件中,接下来看整个流程

初次渲染

首先创建根组件实例和根组件的Render Watcher,然后执行根组件的render函数创建VNode,当遇到keep-alive组件时,会给keep-alive创建一个组件VNode;并创建组件A的组件VNode(插槽原理一篇中说过这种方式),将组件A的组件VNode添加到keep-alive的组件VNode的componentOptions.children中。接下来执行根组件的 patch 过程并为keep-alive组件创建Vue实例。在创建实例过程中,会调用initLifecycle方法,方法内有这样的逻辑

export function initLifecycle (vm: Component) {
  const options = vm.$options
  // 子组件 Vue 实例的 options 才会有 parent 属性,属性值为上一个 Vue 实例
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
  vm.$parent = parent
  vm.$root = parent ? parent.$root : vm
  // ...
  
}

这块逻辑的主要目的是让组件实例建立父子关系。如果当前组件实例的options.abstract不为true则将当前组件实例添加到options.abstract不为true的父辈实例的$children

  • 根组件的Vue实例vm.$parentnull
  • 由于keep-alive组件定义中abstracttrue,不会将自身的Vue实例添加到父级实例的$children中,但是自身的Vue实例的$parent属性依然指向父级实例。而子组件(A)在创建Vue实例时,由于判断条件,会将自身添加到keep-alive组件实例的父级实例的$children

如下图,keep-alive的Vue实例的$parent指向根实例,keep-alive的Vue实例的$children为空;组件A的Vue实例的$parent指向根实例;根实例的$parent指向null,根实例的$children包含组件A的Vue实例

init2.jpg

接下来会调用keep-alivecreated生命周期

created () {
  this.cache = Object.create(null)
  this.keys = []
}

keep-alivecreated 生命周期里定义了 this.cachethis.keys,用于存放缓存的组件,具体作用后面会说

keep-alive的Vue实例创建完成之后,会执行keep-alive组件的自定义render函数创建渲染VNode

render () {
  const slot = this.$slots.default // 获取插槽内容
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    // 获取当前组件的名称,如果没有设置name属性则使用标签名
    const name: ?string = getComponentName(componentOptions)
    const { include, exclude } = this
    if (
      (include && (!name || !matches(include, name))) ||
      (exclude && name && matches(exclude, name))
    ) {
      return vnode
    }

    const { cache, keys } = this
    const key = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}

首先通过this.$slots.default获取插槽内容;调用getFirstComponentChild函数获取第一个组件VNode(注意是组件VNode),也就是demo中组件A的组件VNode;根据组件A的组件VNode获取componentOptions

然后获取当前组件的名称,如果没有设置name属性则使用标签名,接下来通过matches方法判断当前组件的名称和 includeexclude 的关系

function matches (pattern: string | RegExp | Array<string>, name: string): boolean {
  if (Array.isArray(pattern)) {
    return pattern.indexOf(name) > -1
  } else if (typeof pattern === 'string') {
    return pattern.split(',').indexOf(name) > -1
  } else if (isRegExp(pattern)) {
    return pattern.test(name)
  }
  return false
}

通过matches方法可以看出includeexclude 两个属性可以是数组、逗号分隔的字符串、正则表达式

如果当前组件名称不在include中或者在exclude中,则返回当前组件的VNode;否则执行缓存流程

缓存流程如下

render () {
  const slot = this.$slots.default // 获取插槽内容
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    // ...

    const { cache, keys } = this
    const key = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}

创建一个key变量,如果vnode.key不为nullkey的值就是vnode.key,反之变量值是一个拼接字符串。然后拿着这个变量值去cache属性中查找,如果没有则将当前VNode存入cache中,将key存入keys中;如果传入的max小于keys的长度,调用pruneCacheEntry方法,删除keys数组中第一个元素对应的VNode,目的是清除缓存

function pruneCacheEntry (
  cache: VNodeCache,
  key: string,
  keys: Array<string>,
  current?: VNode
) {
  const cached = cache[key]
  // cached 有值,并且 不是当前正在渲染的 组件
  if (cached && (!current || cached.tag !== current.tag)) {
    cached.componentInstance.$destroy()
  }
  cache[key] = null
  remove(keys, key)
}

pruneCacheEntry方法会根据传入的key获取cache中对应的VNode,如果这个VNode不是正在渲染的VNode则调用这个VNode实例的$destroy()方法卸载这个组件。这是一种LRU缓存策略,keys数组的第一个元素对应的VNode可以理解为使用率最低的VNode;因为在render函数中如果命中缓存,会将这个keykeys中取出并重新添加到最后

回到render函数,缓存添加完成之后设置vnode.data.keepAlive = true,然后返回VNode;这个VNode就是demo中A组件的组件VNode。接着进入keep-alive的patch过程,在patch过程中会创建A组件的Vue实例并将实例挂载到vnode.componentInstance中,然后创建A组件的渲染VNode,当整个DOM树创建完成并插入到页面中后,会执行在整个 patch 过程收集的insert钩子函数,其中就包含组件Ainsert钩子函数

insert (vnode: MountedComponentVNode) {
 // vnode.context 指向创建该 组件VNode 时所在的 Vue 实例
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
    componentInstance._isMounted = true
    callHook(componentInstance, 'mounted')
  }
  if (vnode.data.keepAlive) {
    if (context._isMounted) {
      queueActivatedComponent(componentInstance)
    } else {
      activateChildComponent(componentInstance, true /* direct */)
    }
  }
}

insert方法会先调用A组件的mounted生命周期,然后因为A组件的vnode.data.keepAlivetrue但是此时父组件实例(在这里是根实例,因为组件A的组件VNode是在根实例中创建的)的mounted生命周期还没触发,所以不会调用queueActivatedComponent方法,而是调用activateChildComponent(componentInstance, true)方法

export function activateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = false
    if (isInInactiveTree(vm)) {
      return
    }
  } else if (vm._directInactive) {
    return
  }
  if (vm._inactive || vm._inactive === null) {
    vm._inactive = false
    for (let i = 0; i < vm.$children.length; i++) {
      activateChildComponent(vm.$children[i])
    }
    callHook(vm, 'activated')
  }
}

activateChildComponent方法逻辑如下

  • 如果directtrue,设置vm._directInactive = false;调用isInInactiveTree方法,判断传入组件实例的所有祖辈实例的_inactive属性是不是为true
    • 如果有一个为true,说明这个祖辈实例也是包在keep-alive内并且不是活跃状态直接return也就是说如果当前实例的祖辈实例不是活跃状态,则不会调用当前实例的activated生命周期
    • 反之如果所有祖辈实例的_inactive属性为false或者没有此属性,则给组件A的实例添加_inactive属性,设置为false表示此组件是活跃状态。接着遍历A组件实例的所有子孙实例,如果子孙实例中有keep-alive则触发对应子孙组件的activated生命周期,然后触发自身的activated生命周期,所以 activated执行顺序是先子后父
  • 如果directfalse并且vm._directInactivetrue,直接返回

首次渲染 B 组件

当点击demo中的按钮调用change方法

change () {
  this.com = this.com === 'A' ? 'B' : 'A'
}

先看下整体更新流程,修改com属性,通过nextTick将根组件的Watcher更新放到下个队列中。在创建根组件的渲染VNode过程中,会创建组件B的组件VNode。接着进入根组件的 patch 过程,在比对到keep-alive时,由于keep-alive是一个组件所以会调用keep-aliveprepatch钩子函数,从而执行updateChildComponent方法,并调用keep-alive$forceUpdate方法更新。具体流程在Vue源码(十)插槽原理中。调用keep-alivewatcher.run方法,执行render函数,返回组件B的组件VNode;接着进入keep-alive的 patch 过程,patch过程中的oldVnode是组件A的组件VNode,而vnode是组件B的组件VNode。由于这两个组件VNode不相同,所以会创建组件B的Vue实例并执行组件B的挂载过程

B组件的渲染VNode挂载完成之后,会回到keep-alivepatch方法,在patch方法中有这样一段逻辑

// destroy old node
if (isDef(parentElm)) {
  removeVnodes([oldVnode], 0, 0)
} else if (isDef(oldVnode.tag)) {
  invokeDestroyHook(oldVnode)
}

此时组件A和组件B都在页面上,所以会调用A组件的destroy钩子函数

destroy (vnode: MountedComponentVNode) {
  const { componentInstance } = vnode
  if (!componentInstance._isDestroyed) {
    if (!vnode.data.keepAlive) {
      componentInstance.$destroy()
    } else {
      deactivateChildComponent(componentInstance, true /* direct */)
    }
  }
}

因为组件A的vnode.data.keepAlivetrue,所以会调用deactivateChildComponent方法,而不是卸载A组件

export function deactivateChildComponent (vm: Component, direct?: boolean) {
  if (direct) {
    vm._directInactive = true
    if (isInInactiveTree(vm)) {
      return
    }
  }
  if (!vm._inactive) {
    vm._inactive = true
    // 自身变为不活跃时,触发所有 子vm 中 keep-alive 组件的 deactivated 钩子
    for (let i = 0; i < vm.$children.length; i++) {
      deactivateChildComponent(vm.$children[i])
    }
    callHook(vm, 'deactivated')
  }
}

_directInactive属性设置为true,并遍历祖辈实例的_inactive是否为true,即是否为失活状态,如果是则直接返回;否则如果组件实例的_inactive属性为false,说明是活跃状态,将组件实例的_inactive设置为true;并遍历子孙组件,如果子孙组件中有keep-alive,并且当前状态是活跃状态,则调用子孙组件的deactivated生命周期,然后调用自身的deactivated生命周期。

当整个DOM树创建挂载完成之后,会调用组件Binsert方法

insert (vnode: MountedComponentVNode) {
  const { context, componentInstance } = vnode
  if (!componentInstance._isMounted) {
    componentInstance._isMounted = true
    callHook(componentInstance, 'mounted')
  }
  if (vnode.data.keepAlive) {
    if (context._isMounted) {
      queueActivatedComponent(componentInstance)
    } else {
      activateChildComponent(componentInstance, true /* direct */)
    }
  }
},

因为父组件(这里也是根组件,因为组件B的组件VNode也是在根组件中创建的)已经挂载过(在首次创建过程中执行过mounted生命周期),所以执行的是queueActivatedComponent方法

export function queueActivatedComponent (vm: Component) {
  vm._inactive = false
  activatedChildren.push(vm)
}

queueActivatedComponent方法将实例的_inactive设置成false表示活跃组件,并将组件添加到activatedChildren中。也就是说如果子孙组件中也包含keep-alive,并且当前子孙组件的mounted执行过的话,在调用被包裹组件的insert钩子函数时,都不是直接触发activated生命周期,而是将这个被包裹组件的Vue实例添加到activatedChildren中,后面会说为什么要这样做。

渲染Watcher的更新是通过flushSchedulerQueue方法触发的,flushSchedulerQueue方法内会循环队列中的所有Watcher并调用watcher.run方法,所以渲染完成后回到此方法继续执行

function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true
  let watcher, id

  queue.sort((a, b) => a.id - b.id)

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn()
        break
      }
    }
  }
  // 从这里开始
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()
  // 重置所有状态
  resetSchedulerState()

  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)
}

调用callActivatedHooks方法,并将activatedQueue传入,activatedQueue内保存的是所有子孙keep-alive包裹的组件实例

function callActivatedHooks (queue) {
  for (let i = 0; i < queue.length; i++) {
    queue[i]._inactive = true
    activateChildComponent(queue[i], true /* true */)
  }
}

遍历所有实例,并将_inactive置为true,然后调用activateChildComponent去调用所有实例的activated生命周期

这里有个疑问就是为什么要放在最后统一执行,而不是和mounted一样在insert钩子函数中执行?

这是因为在activateChildComponent方法中有一个判断isInInactiveTree(vm),如果不提前将父辈中被keep-alive组件包裹的组件实例的_inactive属性置为false的话,会直接返回而不是执行activated生命周期函数。

缓存组件

当再次点击按钮触发更新,创建keep-alive的组件VNode,并创建A组件的组件VNode,VNode创建完成之后,进入根组件的 patch 阶段。当遇到keep-alive的组件VNode时,调用keep-alive的组件VNode的 prepatch钩子函数,由于keep-alive的组件VNode中有插槽节点,调用$forceUpdate更新keep-laive组件。执行keep-aliverender函数

此时A组件的组件VNode已经缓存了,从缓存的组件VNode获取Vue实例,将实例挂载到组件A的组件VNode上;然后更新keys,组件A对应的key重新添加到keys最后

render () {
  const slot = this.$slots.default
  const vnode: VNode = getFirstComponentChild(slot)
  const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
  if (componentOptions) {
    const name: ?string = getComponentName(componentOptions)
    const { include, exclude } = this
    if (
      (include && (!name || !matches(include, name))) ||
      (exclude && name && matches(exclude, name))
    ) {
      return vnode
    }

    const { cache, keys } = this
    const key: ?string = vnode.key == null
      ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
    : vnode.key
    if (cache[key]) {
      vnode.componentInstance = cache[key].componentInstance
      remove(keys, key)
      keys.push(key)
    } else {
      cache[key] = vnode
      keys.push(key)
      if (this.max && keys.length > parseInt(this.max)) {
        pruneCacheEntry(cache, keys[0], keys, this._vnode)
      }
    }
    vnode.data.keepAlive = true
  }
  return vnode || (slot && slot[0])
}

返回组件A的组件Vnode之后,进入keep-alive的 patch 过程,此时的oldVnode是组件B的组件VNode,newVnode是组件A的组件VNode。由于新老VNode不同,所以会调用createElm函数,createElm函数内当遇到组件Vnode时,会调用createComponent方法

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i = vnode.data
  if (isDef(i)) {
    // 判断组件实例是否已经存在, 并且组件被 keep-alive 包裹
    const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
    if (isDef(i = i.hook) && isDef(i = i.init)) {
      // 执行 组件的 init 钩子函数
      i(vnode, false /* hydrating */)
    }
    if (isDef(vnode.componentInstance)) {
      initComponent(vnode, insertedVnodeQueue)
      insert(parentElm, vnode.elm, refElm)
      if (isTrue(isReactivated)) {
        reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
      }
      return true
    }
  }
}

会调用A组件的init钩子函数

init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
  if (
    vnode.componentInstance &&
    !vnode.componentInstance._isDestroyed &&
    vnode.data.keepAlive
  ) {
    const mountedNode: any = vnode
    componentVNodeHooks.prepatch(mountedNode, mountedNode)
  } else {
    const child = vnode.componentInstance = createComponentInstanceForVnode(
      vnode,
      activeInstance
    )
    child.$mount(hydrating ? vnode.elm : undefined, hydrating)
  }
},

此时,iftrue,所以会调用A组件的prepatch钩子函数,而不是用createComponentInstanceForVnode重新创建Vue实例,所以不会调用created生命周期,并且不会再次执行挂载过程

prepatch (oldVnode: MountedComponentVNode, vnode: MountedComponentVNode) {
  const options = vnode.componentOptions
  const child = vnode.componentInstance = oldVnode.componentInstance
  updateChildComponent(
    child,
    options.propsData, // updated props 传入子组件的最新的 props 值
    options.listeners, // updated listeners 自定义事件
    vnode, // new parent vnode
    options.children // 最新的插槽VNode
  )
},

prepatch内调用updateChildComponent方法,在当前例子中,组件A没有更改所以不会触发组件A的更新。回到createComponent继续执行,触发initComponent方法更新组件A的组件VNode的DOM结构;并收集组件Ainsert钩子函数。通过insert方法,将DOM插入到目标位置;此时页面上组件A和组件B都存在。

// createComponent 内部
if (isDef(vnode.componentInstance)) {
  initComponent(vnode, insertedVnodeQueue)
  insert(parentElm, vnode.elm, refElm)
  if (isTrue(isReactivated)) {
    reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
  }
  return true
}

// initComponent
  function initComponent (vnode, insertedVnodeQueue) {
    if (isDef(vnode.data.pendingInsert)) {
      insertedVnodeQueue.push.apply(insertedVnodeQueue, vnode.data.pendingInsert)
      vnode.data.pendingInsert = null
    }
    // 将 渲染vnode 的 $el 属性赋值给 组件vnode 的 elm 属性
    vnode.elm = vnode.componentInstance.$el
    if (isPatchable(vnode)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
      setScope(vnode)
    } else {
      registerRef(vnode)
      insertedVnodeQueue.push(vnode)
    }
  }

由于isReactivatedtrue,还会执行reactivateComponent方法,方法内部解决对重新活跃组件 transition 动画不触发的问题,然后再执行一次insert方法,这里有点奇怪感觉,不知道为啥还要执行一次insert

function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
  let i
  // hack for #4339: a reactivated component with inner transition
  // does not trigger because the inner node's created hooks are not called
  // again. It's not ideal to involve module-specific logic in here but
  // there doesn't seem to be a better way to do it.
  let innerNode = vnode
  while (innerNode.componentInstance) {
    innerNode = innerNode.componentInstance._vnode
    if (isDef(i = innerNode.data) && isDef(i = i.transition)) {
      for (i = 0; i < cbs.activate.length; ++i) {
        cbs.activate[i](emptyNode, innerNode)
      }
      insertedVnodeQueue.push(innerNode)
      break
    }
  }
  // unlike a newly created component,
  // a reactivated keep-alive component doesn't insert itself
  insert(parentElm, vnode.elm, refElm)
}

插入完成之后,剩下逻辑就和上面第二次点击按钮一样了。就是在keep-alivepatch方法最后,会将组件B删除并调用deactivated生命周期函数。然后调用组件Ainsert钩子函数,由于组件A已经挂载过所以不会执行mounted生命周期;而是将Vue实例添加到activatedChildren中,并将_inactive属性置为false。当所有组件都渲染完成之后,回到flushSchedulerQueue中,调用callActivatedHooks方法触发activatedChildren中所有元素的activated生命周期函数。

总结

keep-alive 原理

keep-alive使用插槽的方式;在创建父组件VNode时,对被包裹组件创建组件VNode。当调用keep-aliverender函数时,获取默认的插槽VNode,然后判断这个组件VNode有没有缓存过:

  • 如果缓存过,则给这个组件VNode设置缓存的Vue实例,防止再次创建;也就是说keep-alive缓存的是组件的Vue实例,每次命中缓存后,都不需要再次创建Vue实例,而是用之前缓存的实例。最后返回这个组件VNode。
  • 如果没有缓存过,使用LRU缓存策略添加缓存。最后返回这个组件VNode。