# 简述

首先keep-alive是一个组件,但是它不会渲染dom,有三个属性参数include, exclude, max。分别代表着缓存白名单,缓存黑名单,以及缓存组件最大数。max的作用是当缓存组件超出max值时,可以优化缓存,让缓存以LRU最近最久未使用的策略去删除多余的组件

# 组件

keep-alive组件大致长这样

// src/core/components/keep-alive.js
export default {
  name: 'keep-alive',
  abstract: true, // 判断当前组件虚拟dom是否渲染成真实dom的关键
  props: {
      include: patternTypes, // 缓存白名单
      exclude: patternTypes, // 缓存黑名单
      max: [String, Number] // 缓存的组件
  },
  created() {
     this.cache = Object.create(null) // 缓存虚拟dom
     this.keys = [] // 缓存的虚拟dom的键集合
  },
  destroyed() {
    for (const key in this.cache) {
       // 删除所有的缓存
       pruneCacheEntry(this.cache, key, this.keys)
    }
  },
 mounted() {
   // 实时监听黑白名单的变动
   this.$watch('include', val => {
       pruneCache(this, name => matched(val, name))
   })
   this.$watch('exclude', val => {
       pruneCache(this, name => !matches(val, name))
   })
 },

  render () {
    const slot = this.$slots.default
    const vnode: VNode = getFirstComponentChild(slot) // 找到第一个组件对象
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) { // 存在组件参数
      // check pattern
      const name: ?string = getComponentName(componentOptions) // 组件名
      const { include, exclude } = this
      if ( // 根据include,exclude条件匹配是否缓存数组,不缓存直接返回vnode组件实例
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode
      }

      const { cache, keys } = this
      // 根据组件的id和tag定义组件的缓存key
      const key: ?string = vnode.key == null
        // 同一个构造函数可能会被注册为不同的本地组件,所以单独使用cid是不够的  
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) { // 已经缓存过改组件
        vnode.componentInstance = cache[key].componentInstance
        // 让当前的key始终保持最优先,更新的原因是为了实现LRU置换策略
        // LRU: 最近最少使用策略, 选择最近最久未使用的页面予以淘汰
        // 假设缓存时发现超过了max缓存数量,则根据LRU删除最近最久未使用的实例(即是下标为0的那个key),因为当前活跃的缓存组件已经被更改到数组中的最后了
        remove(keys, key) // 从缓存的虚拟dom键中删除key
        keys.push(key) // 删除完又添加,也就是调整排序
      } else {
        // delay setting the cache until update
        // 推迟设置缓存直到更新
        this.vnodeToCache = vnode
        this.keyToCache = key
      }

      // 将该组件实例的keepAlive属性设置为true
      vnode.data.keepAlive = true // 渲染和执行被包裹组件的钩子函数需要用到
    }
    return vnode || (slot && slot[0])
  }
}

# render函数的作用

  • 首先找到keep-alive包裹的第一个组件对象
  • 根据include和exclude条件匹配是否缓存组件
  • 根据组件的cid和tag定义组件的缓存key,因为单独的cid并不能保持唯一性,同一个构造函数可能会被注册为不同的本地组件。根据key查找是否缓存了该组件,如果是取出组件更新位置,当前组件已经缓存过,则让该组件放到缓存数组中的最后一个,如果设置了max属性,会因为LRU策略,删除下标为0(最近最久未使用)的组件。
  • 设置当前组件keepAlive属性为true,作用是可以让初始化生命周期时keep-alive不被渲染

# 渲染

先看一张vue渲染导图

Vue的渲染时从render阶段开始的,但keep-alive的渲染是在patch阶段,这是构建组件树(虚拟dom树),并将vnode转化成DOM结点的过程。

简述从renderpatch过程

从new Vue开始

import App from './App.vue'

new Vue({
    render: h => h(App)
}).$mount('#app')
  • Vue在渲染的时候先调用原型上的_render函数将组件对象转化成一个VNode实例;而_render是通过调用createElement和createEmptyVNode两个函数进行转化;
  • createElement的转化过程会根据不同的情形选择new VNode或者调用createComponent函数做VNode实例化;
  • 完成VNode实例化后,这时候Vue调用原型上的_update函数把VNode渲染成真实DOM,这个过程又是通过调用patch函数完成的(这就是patch阶段了)

# keep-alive组件的渲染

keep-alive不会生成真正的结点

// src/core/instance/lifecycle.js
export function initLifecycle (vm: Component) {
  const options = vm.$options

  // 找到第一个非abstract的父组件实例
  // Vue在初始化生命周期的时候,为组件实例建立父子关系会根据abstract属性决定是否忽略某个组件
  // 在keep-alive中,设置了abstract:true,那Vue就会跳过该组件实例
  // 最后构建的组件树中就不会包含keep-alive组件
  // 那么由组件树渲染成的DOM树自然也不会有keep-alive相关的节点了
  let parent = options.parent
  if (parent && !options.abstract) {
    while (parent.$options.abstract && parent.$parent) {
      parent = parent.$parent
    }
    parent.$children.push(vm)
  }
}

# keep-alive包裹的组件是如何使用缓存的

  // src/core/vdom/patch.js
  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    let i = vnode.data
    if (isDef(i)) {
      const isReactivated = isDef(vnode.componentInstance) && i.keepAlive
      if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
      }
      // 在调用init钩子之后,如果vnode是一个子组件,
      // 它应该创建一个子实例并挂载它。 子组件还设置了占位符vnode的elm。
      // 在这种情况下,我们只需要返回元素就可以了。 

      // 在首次加载被包裹组建时,由keep-alive.js中的render函数可知,
      // vnode.componentInstance的值是undfined,keepAlive的值是true,
      // 因为keep-alive组件作为父组件,它的render函数会先于被包裹组件执行;
      // 那么只执行到i(vnode,false),后面的逻辑不执行;

      // 再次访问被包裹组件时,vnode.componentInstance的值就是已经缓存的组件实例,
      // 那么会执行insert(parentElm, vnode.elm, refElm)逻辑,
      // 这样就直接把上一次的DOM插入到父元素中。
      if (isDef(vnode.componentInstance)) {
        initComponent(vnode, insertedVnodeQueue)
        insert(parentElm, vnode.elm, refElm) // keep-alive将缓存的DOM(vnode.elem) 插入父元素中
        if (isTrue(isReactivated)) {
          reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm)
        }
        return true
      }
    }
  }

# 只执行一次的钩子

// src/core/vdom/create-component.js
const componentVNodeHooks = {
  init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
    if ( // 当组件的vnode.componentInstance和vnode.data.keepAlive同时存在时,就不进行$mount
      // 过程,也就不会执行组件的所有钩子函数(beforeCreate、created、mounted等)
      vnode.componentInstance &&
      !vnode.componentInstance._isDestroyed &&
      vnode.data.keepAlive
    ) {
      // kept-alive components, treat as a patch
      const mountedNode: any = vnode // work around flow
      componentVNodeHooks.prepatch(mountedNode, mountedNode)
    } else {
      const child = vnode.componentInstance = createComponentInstanceForVnode(
        vnode,
        activeInstance
      )
      child.$mount(hydrating ? vnode.elm : undefined, hydrating)
    }
  },
}

# 可重复的activated

在patch的阶段,最后会执行invokelinsertHook函数,而这个函数就是去调用组件实例自身的insert钩子

// src/core/vdom/patch.js
function invokeInsertHook (vnode, queue, initial) {
  if (isTrue(initial) && isDef(vnode.parent)) {
      vnode.parent.data,pendingInsert = queue
  } else {
    for(let i =0; i<queue.length; ++i) {
      queue[i].data.hook.insert(queue[i]) // 调用VNode自身的insert钩子函数
    }
  }
}

再看insert钩子:

const componentVNodeHooks = {
  // init()
  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 */)
      }
    }
    // ...
  }
}

在这个钩子里面,调用了activateChildComponent函数递归地去执行所有子组件的activated钩子函数:

// src/core/instance/lifecycle.js
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')
  }
}

相反地,deactivated钩子函数也是一样的原理,在组件实例(VNode)的destroy钩子函数中调用deactivateChildComponent函数。