keep-alive

keep-alive 是什么

1.keep-alive是vue框架自身的组件

2.keep-alive组件自身不会渲染一个DOM元素,也不会出现在父组件链中;使用keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们

keep-alive 的用法

1.在动态组件中应用

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <component :is="currentComponent"></component>
</keep-alive>复制代码

2.在 vue-Router 中应用

<keep-alive :include="whiteList" :exclude="blackList" :max="amount">
  <router-view></router-view>
</keep-alive>复制代码

keep-alive 组件提供的 include 属性和 exculde 属性可以定义缓存名单,确定被 keep-alive 包围的组件哪些被缓存,哪些不被缓存,其中 include 定义缓存白名单,exclude 定义缓存黑名单。

vue基础知识铺垫

在学习vue keep-alive的缓存原理之前,我们需要了解vue组件的渲染原理,这样才能知道keep-alive的缓存时机和原理,我们首先要搞清楚两个问题。

1.什么是虚拟节点Virtual Vnode?它的作用?

2.vue组件的渲染过程

接下来,让我们先来了解一下Virtual Vnode。

1.什么是虚拟节点Virtual Vnode?它的作用?

Virtual DOM的诞生是建立在浏览器DOM操作是很“昂贵”的基础之上的,下面我们来直观看一下DOM元素(下图),这仅仅是一个最简单的DOM元素。当我们通过JavaScript操作页面中DOM节点时,浏览器会从构建DOM树开始从头到尾执行一遍渲染流程。 假如我们要更新10个DOM节点, 浏览器接收到第一个更新请求时会马上执行, 连续执行10次,而事实上最后一次执行的结果才是我们需要的。前九次运算都是在浪费性能,最终导致页面卡顿,内存占用过高,用户体验差。keep-alive实现原理分析_keep-alive我们都知道,在做前端项目时,需要根据用户的选择,不断的更新页面视图,以响应用户的操作,但是DOM操作是十分浪费性能的,虚拟DOM就是为了解决浏览器性能问题而被设计出来的。 假如一次操作中有10个更新DOM的动作,虚拟DOM不会立即执行,而是将这10次更新的diff内容保存到本地的JS对象中,最终将这个JS对象一次性更新到DOM树上避免大量无效计算。这样页面的更新可以全部反应在JS对象(虚拟DOM上),操作内存中的JS对象速度当然要更快,等更新完后再将JS对象映射成真实的DOM,交给浏览器去绘制。

总结一下,Virtual DOM就是用一个原生的JS对象去描述DOM节点,修改它比直接修改一个DOM的代价要小很多。在Vue中,Virtual DOM是用VNode(一个Class)去描述的,它的核心定义无非就几个关键属性,标签名、数据、子节点、键值等,其它属性都是用来扩展 VNode的灵活性以及实现一些特殊feature的。由于VNode只是用来映射到真实DOM的渲染,不需要包含操作DOM的方法,因此它是非常轻量和简单的。

Virtual DOM除了它的数据结构的定义,映射到真实的DOM实际上要经历VNode的create、diff、patch等过程,有了Virtual DOM,就可以通过diff算法,对照哪些vue节点更新了,然后统一去更新有变化的节点,减少定位时间和重绘过程,提升性能!

2.vue组件的渲染过程

vue就是基于Virtual DOM实现页面渲染和更新的,基于Virtual DOM机制的页面渲染机制--首先将数据渲染为Virtual DOM,然后将Virtual DOM生成DOM,有数据更新时,先对照Virtual DOM的不同,然后统一对DOM元素做修改,这就是总体思想。 接下来,我们来看下大概的流程:

1.整体的渲染过程

keep-alive实现原理分析_keep-alive_02整体的渲染过程,其实跟上面总结的总体思路是一一对应的,大概分为三步:

1.$mount过程--根据template生成render()函数。

2.render过程--生成虚拟的Vnode。

3.patch过程--将虚拟节点转换为真实的DOM。

4.整个1、2、3三个步骤,是根据template模版结构,深度优先遍历循环的,父节点先于子节点渲染,子节点先于父节点插入DOM。

下面我们来详细了解下三个过程

2.$mount过程

keep-alive实现原理分析_keep-alive_03整个$mount过程就是把template模版转换为能生成Vnode节点的renger()函数,分为三步:

1.parse--解析模版字符串生成AST

parse的目标是把template模板字符串转换成AST树,它是一种用JavaScript对象的形式来描述整个模板。那么整个parse的过程是利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造AST树的目的。个人理解就是把template(模板)解析成一个对象,该对象是包含这个模板所以信息的一种数据,而这种数据浏览器是不支持的,为Vue后面的处理template提供基础数据。本实例中会生成如下AST树。

2.optimize--优化AST语法树 为什么此处会有优化过程?我们知道Vue是数据驱动,是响应式的,但是template模版中并不是所有的数据都是响应式的,也有许多数据是初始化渲染之后就不会有变化的,那么这部分数据对应的DOM也不会发生变化。后面有一个update更新界面的过程,在这当中会有一个patch的过程,diff算法会直接跳过静态节点,从而减少了比较的过程,优化了patch的性能。

3.codegen--将优化后的AST树转换成可执行的代码,生成render函数。

3.生成真实DOM的过程(patch)

下面我们来了解一下patch的过程,keep-alive组件的缓存实现就是在patch的过程当中生效的。 patch过程的主要逻辑就是要把vnode节点转化为真实的DOM元素并插入到html中,其主要逻辑是在createComponent()函数中开始的:

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) { // 判断init()钩子函数是否存在
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; // isReactivated用来判断组件是否被keep-alive缓存
        if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */); // 执行组件初始化的内部钩子init(),开启深度优先遍历
        }
        if (isDef(vnode.componentInstance)) { // 所有组件的深度优先遍历完成以后,开始执行将DOM插入到父组件的过程,从最底层的DOM开始,向父级元素传递
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm); // 将真实节点添加到父节点中
          if (isTrue(isReactivated)) { 
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm); // 如果是keep-alive缓存的组件,则直接将DOM元素插入父级DOM,不需要在重新渲染
          }
          return true
      }
   }
}复制代码

可以看到createComponent会判断当前标签是不是组件,如果是就会执行组件的init()钩子函数,我们在来看一下init()钩子函数,先不考虑keepAlive,在init()函数中,会先判断当前组件是否存在子组件,如果存在,就会调用child.mount()函数,执行子组件的渲染和转换过程,所以说整个mount和patch过程是深度优先遍历的。

// 组件内部钩子,createComponent的i就是componentVNodeHooks对象,可以看到i(vnode, false /* hydrating */);调用就是执行组件的实例话
var componentVNodeHooks = {
   init: function init (vnode, hydrating) {
     if (
       vnode.componentInstance &&
       !vnode.componentInstance._isDestroyed &&
       vnode.data.keepAlive
     ) { // 判断是否已被keep-alive缓存
       var mountedNode = vnode; // work around flow
       componentVNodeHooks.prepatch(mountedNode, mountedNode); // 执行一些预操作
     } else { // 如果没keep-alive缓存,被就执行组件的实例化,深度优先遍历
       var child = vnode.componentInstance = createComponentInstanceForVnode(
         vnode,
         activeInstance
       );
       child.$mount(hydrating ? vnode.elm : undefined, hydrating);
     }
   }
}复制代码

大家如果想要了解更多关于vue渲染的原理,可以参考这个网站:caibaojian.com/vue-analysi…。 整个patch的过程可以分为三个步骤: 1.执行当前组件的createComponent函数,并调用当前组件的init()函数,判断当前组件是否存在子组件,如果存在,则执行第二步。 2.执行子组件的$mount--render()--patch()--createComponent()过程,并判断是否存在子组件,如果存在,则反复执行第二步,直到遍历完所有子组件,开始执行第三步。 3.执行createComponent函数中的initComponent(vnode, insertedVnodeQueue)和insert(parentElm, vnode.elm, refElm)函数,生成并将DOM插入真实html中。

在了解了vue组件的渲染过程以后,我们再来看keep-alive的缓存原理就会简单一些。

keep-alive源码+举例分析

1.keep-alive源码

keep-alive是vue框架内部自己实现的组件,通过上个章节对vue组件渲染过程的简单总结,我们知道组件的渲染过程(render)是先父后子的关系,所以被keep-alive组件(父组件)包裹的子组件的渲染过程是在keep-alive(父组件)组件render之后的进行的,下面我们结合keep-alive的源码,看一下keep-alive组件的render过程

export default {
  name: 'keep-alive', // 组件名称
  abstract: true, // 抽象组件,不会真正的渲染成DOM

  props: {
    include: patternTypes, // 需要缓存的组件
    exclude: patternTypes, // 不需要缓存的组件
    max: [String, Number] // 缓存的最大个数
  },

  created () {
    this.cache = Object.create(null) // 存储被缓存的组件
    this.keys = []
  },

  destroyed () {
    for (const key in this.cache) {
      pruneCacheEntry(this.cache, key, this.keys) // 修剪缓存
    }
  },

  mounted () {
    this.$watch('include', val => {
      pruneCache(this, name => matches(val, name))
    })
    this.$watch('exclude', val => {
      pruneCache(this, name => !matches(val, name))
    })
  },

  render () {
    const slot = this.$slots.default 
    const vnode: VNode = getFirstComponentChild(slot) // 通过slot获取缓存组件
    const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions
    if (componentOptions) {
      // check pattern
      const name: ?string = getComponentName(componentOptions)
      const { include, exclude } = this
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))
      ) {
        return vnode // 据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode)
      }

      const { cache, keys } = this
      const key: ?string = vnode.key == null
        ? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
        : vnode.key
      if (cache[key]) { // 如果cache[key]有值则表示有缓存,非首次渲染
        vnode.componentInstance = cache[key].componentInstance // 如果命中缓存,则将缓存组件的实例赋值给keep-alive组件的componentInstance属性,后续有用
        // make current key freshest
        remove(keys, key)
        keys.push(key)
      } else { // 如果cache[key]没有值则表示首次渲染,并记录缓存的组件
        cache[key] = vnode
        keys.push(key)
        // prune oldest entry
        if (this.max && keys.length > parseInt(this.max)) {
          pruneCacheEntry(cache, keys[0], keys, this._vnode)
        }
      }

      vnode.data.keepAlive = true
    }
    return vnode || (slot && slot[0]) // 最终返回keep-alive组件
  }
}复制代码

keep-alive组件有自己的渲染函数,下面我们来看一下keep-alive的渲染函数render都做了哪些操作:

第一步:获取keep-alive包裹着的第一个子组件对象及其组件名;

第二步:根据设定的黑白名单(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例(VNode),否则执行第三步;

第三步:根据组件ID和tag生成缓存Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该key在this.keys中的位置(更新key的位置是实现LRU置换策略的关键),否则执行第四步;

第四步:在this.cache对象中存储该组件实例并保存key值,之后检查缓存的实例数量是否超过max的设置值,超过则根据LRU置换策略删除最近最久未使用的实例(即是下标为0的那个key)。

第五步:最后并且很重要,将该组件实例的keepAlive属性值设置为true。

第六步:完成render过程以后,就该执行组件的patch过程,将组件变成实际的dom,下面我们结合例子来分析一下,keep-alive组件的patch过程。

1.例子

let A = {
    template: '<div class="a">' +
        '<p>A组件</p>' +
            '</div>',
        name: 'A'
    }

let B = {
    template: '<div class="b">' +
        '<p>B组件</p>' +
            '</div>',
        name: 'B'
    }

new Vue({
    el: '#app',
    template: '<div class="content">' +
        '<keep-alive>' +
        '<component :is="currentComp">' +
        '</component>' +
        '</keep-alive>' +
        '<button @click="change">switch</button>' +
        '</div>',
    data: {
        currentComp: 'A'
    },
    methods: {
        change() {
            this.currentComp = this.currentComp === 'A' ? 'B' : 'A'
        }
    },
    components: {
        A,
        B
    }
})复制代码

以A两个组件为例,A组件被keep-alive缓存,结合源码,我们对比看一下A组件在首次显示(首次渲染)和切换再次显示(缓存渲染),keep-alive是怎么实现组件缓存的。

首次渲染

第一步,keep-alive组件执行render()函数,可以看到首次执行render()函数的时候,没有缓存任何子组件,keep-alive缓存的子组件的vnode.componentInstance为null且vnode.data.keepAlive为true。

keep-alive实现原理分析_keep-alive_04

第二步,keep-alive组件的patch()过程,实际上执行的是init()函数

keep-alive实现原理分析_keep-alive_05

第三步,我们来看下init()函数,在init()函数里面会对vnode.componentInstance和vnode.data.keepAlive的值做判断,由第一步的值可知,首次渲染的时候会执行createComponentInstanceForVnode()创建新的子组件,接下来会去执行子组件的$mount过程,去创建子组件的虚拟Vnode。

keep-alive实现原理分析_keep-alive_06

第四步,可以看到$mount过程生成了,接下来回去循环挂载子组件。

keep-alive实现原理分析_keep-alive_07

非首次渲染

当我们点击切换按钮,从A组件重新切回B组件的时候,就会触发keep-alive缓存机制,我们来看一下整个流程。

第一步,触发keep-alive的render()函数,此时会命中缓存,所以keep-alive缓存的子组件的vnode.componentInstance为组件A且vnode.data.keepAlive为true。

keep-alive实现原理分析_keep-alive_08

第二步,执行keep-alive组件的patch()过程,实际上执行的是init()函数,这时vnode.componentInstance和vnode.data.keepAlive都为true,所以不会执行createComponentInstanceForVnode()创建新的子组件,而是直接去缓存中取子组件。

keep-alive实现原理分析_keep-alive_09

第三步,当我们的代码重新执行到createComponent时,此时isReactivated为true,会执行reactivateComponent()方法

keep-alive实现原理分析_keep-alive_10

第四步,执行reactivateComponent()函数,最后通过执行insert(parentElm, vnode.elm, refElm)就把缓存的DOM对象直接插入到目标元素中,这样就完成了在数据更新的情况下的渲染过程。

keep-alive实现原理分析_keep-alive_11

这就是整个keep-alive实现缓存的原理。