乐趣区

关于vue.js:vue源码分析keepalive原理上

上一节最初略微提到了 Vue 内置组件的相干内容,从这一节开始,将会对某个具体的内置组件进行剖析。首先是 keep-alive,它是咱们日常开发中常常应用的组件,咱们在不同组件间切换时,常常要求放弃组件的状态,以防止反复渲染组件造成的性能损耗,而keep-alive 常常和上一节介绍的动静组件联合起来应用。因为内容过多,keep-alive的源码剖析将分为高低两局部,这一节次要围绕 keep-alive 的首次渲染开展。

13.1 根本用法

keep-alive的应用只须要在动静组件的最外层增加标签即可。

<div id="app">
    <button @click="changeTabs('child1')">child1</button>
    <button @click="changeTabs('child2')">child2</button>
    <keep-alive>
        <component :is="chooseTabs">
        </component>
    </keep-alive>
</div>
var child1 = {template: '<div><button @click="add">add</button><p>{{num}}</p></div>',
    data() {
        return {num: 1}
    },
    methods: {add() {this.num++}
    },
}
var child2 = {template: '<div>child2</div>'}
var vm = new Vue({
    el: '#app',
    components: {
        child1,
        child2,
    },
    data() {
        return {chooseTabs: 'child1',}
    },
    methods: {changeTabs(tab) {this.chooseTabs = tab;}
    }
})

简略的后果如下,动静组件在 child1,child2 之间来回切换,当第二次切到 child1 时,child1保留着原来的数据状态,num = 5

13.2 从模板编译到生成 vnode

依照以往剖析的教训,咱们会从模板的解析开始说起,第一个疑难便是:内置组件和一般组件在编译过程有区别吗?答案是没有的,不论是内置的还是用户定义组件,实质上组件在模板编译成 render 函数的解决形式是统一的,这里的细节不开展剖析,有纳闷的能够参考前几节的原理剖析。最终针对 keep-aliverender函数的后果如下:

with(this){···_c('keep-alive',{attrs:{"include":"child2"}},[_c(chooseTabs,{tag:"component"})],1)}

有了 render 函数,接下来从子开始到父会执行生成 Vnode 对象的过程,_c('keep-alive'···)的解决,会执行 createElement 生成组件 Vnode, 其中因为keep-alive 是组件,所以会调用 createComponent 函数去创立子组件 Vnode,createComponent 之前也有剖析过,这个环节和创立一般组件 Vnode 不同之处在于,keep-aliveVnode 会剔除多余的属性内容,因为 keep-alive 除了 slot 属性之外,其余属性在组件外部并没有意义,例如 class 款式,<keep-alive clas="test"></keep-alive>等,所以在 Vnode 层剔除掉多余的属性是有意义的。而 <keep-alive slot="test"> 的写法在 2.6 以上的版本也曾经被废除。(其中 abstract 作为形象组件的标记,以及其作用咱们前面会讲到)

// 创立子组件 Vnode 过程
function createComponent(Ctordata,context,children,tag) {// abstract 是内置组件 (形象组件) 的标记
    if (isTrue(Ctor.options.abstract)) {
        // 只保留 slot 属性,其余标签属性都被移除,在 vnode 对象上不再存在
        var slot = data.slot;
        data = {};
        if (slot) {data.slot = slot;}
    }
}

13.3 首次渲染

keep-alive之所以特地,是因为它不会反复渲染雷同的组件,只会利用首次渲染保留的缓存去更新节点。所以为了全面理解它的实现原理,咱们须要从 keep-alive 的首次渲染开始说起。

13.3.1 流程

和渲染一般组件雷同的是,Vue会拿到后面生成的 Vnode 对象执行实在节点创立的过程,也就是相熟的 patch 过程,patch执行阶段会调用 createElm 创立实在 dom,在创立节点途中,keep-alivevnode对象会被认定是一个组件 Vnode, 因而针对组件Vnode 又会执行 createComponent 函数,它会对 keep-alive 组件进行初始化和实例化。

function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        // isReactivated 用来判断组件是否缓存。var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
            // 执行组件初始化的外部钩子 init
          i(vnode, false /* hydrating */);
        }
        if (isDef(vnode.componentInstance)) {
          // 其中一个作用是保留实在 dom 到 vnode 中
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm);
          if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }

keep-alive组件会先调用外部钩子 init 办法进行初始化操作,咱们先看看 init 过程做了什么操作。

// 组件外部钩子
var componentVNodeHooks = {init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
          // 将组件实例赋值给 vnode 的 componentInstance 属性
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    },
    // 前面剖析
    prepatch:function() {}
}

第一次执行,很显著组件 vnode 没有 componentInstance 属性,vnode.data.keepAlive也没有值,所以会 调用 createComponentInstanceForVnode 办法进行组件实例化并将组件实例赋值给 vnodecomponentInstance属性, 最终执行组件实例的 $mount 办法进行实例挂载。

createComponentInstanceForVnode就是组件实例化的过程,而组件实例化从系列的第一篇就开始说了,无非就是一系列选项合并,初始化事件,生命周期等初始化操作。

function createComponentInstanceForVnode (vnode, parent) {
    var options = {
      _isComponent: true,
      _parentVnode: vnode,
      parent: parent
    };
    // 内联模板的解决,疏忽这部分代码
    ···
    // 执行 vue 子组件实例化
    return new vnode.componentOptions.Ctor(options)
  }
13.3.2 内置组件选项

咱们在应用组件的时候常常利用对象的模式定义组件选项,包含 data,method,computed 等,并在父组件或根组件中注册。keep-alive同样遵循这个情理,内置两字也阐明了 keep-alive 是在 Vue 源码中内置好的选项配置,并且也曾经注册到全局。

// keepalive 组件选项
  var KeepAlive = {
    name: 'keep-alive',
    // 形象组件的标记
    abstract: true,
    // keep-alive 容许应用的 props
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number]
    },

    created: function created () {
      // 缓存组件 vnode
      this.cache = Object.create(null);
      // 缓存组件名
      this.keys = [];},

    destroyed: function destroyed () {for (var key in this.cache) {pruneCacheEntry(this.cache, key, this.keys);
      }
    },

    mounted: function mounted () {
      var this$1 = this;
      // 动静 include 和 exclude
      // 对 include exclue 的监听
      this.$watch('include', function (val) {pruneCache(this$1, function (name) {return matches(val, name); });
      });
      this.$watch('exclude', function (val) {pruneCache(this$1, function (name) {return !matches(val, name); });
      });
    },
    // keep-alive 的渲染函数
    render: function render () {
      // 拿到 keep-alive 下插槽的值
      var slot = this.$slots.default;
      // 第一个 vnode 节点
      var vnode = getFirstComponentChild(slot);
      // 拿到第一个组件实例
      var componentOptions = vnode && vnode.componentOptions;
      // keep-alive 的第一个子组件实例存在
      if (componentOptions) {
        // check pattern
        // 拿到第一个 vnode 节点的 name
        var name = getComponentName(componentOptions);
        var ref = this;
        var include = ref.include;
        var exclude = ref.exclude;
        // 通过判断子组件是否满足缓存匹配
        if (
          // not included
          (include && (!name || !matches(include, name))) ||
          // excluded
          (exclude && name && matches(exclude, name))
        ) {return vnode}

        var ref$1 = this;
        var cache = ref$1.cache;
        var keys = ref$1.keys;
        var key = vnode.key == null
          ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
          : vnode.key;
          // 再次命中缓存
        if (cache[key]) {vnode.componentInstance = cache[key].componentInstance;
          // make current key freshest
          remove(keys, key);
          keys.push(key);
        } else {
        // 首次渲染时,将 vnode 缓存
          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;
      }
      // 将渲染的 vnode 返回
      return vnode || (slot && slot[0])
    }
  };

keep-alive选项跟咱们平时写的组件选项还是根本相似的,惟一的不同是 keep-ailve 组件没有用 template 而是应用 render 函数。keep-alive实质上只是存缓存和拿缓存的过程,并没有理论的节点渲染,所以应用 render 解决是最优的抉择。

参考 Vue3 源码视频解说:进入学习

13.3.3 缓存 vnode

还是先回到流程图的剖析。下面说到 keep-alive 在执行组件实例化之后会进行组件的挂载。而挂载 $mount 又回到 vm._render(),vm._update() 的过程。因为 keep-alive 领有 render 函数,所以咱们能够间接将焦点放在 render 函数的实现上。

    1. 首先是获取 keep-alive 下插槽的内容,也就是 keep-alive 须要渲染的子组件, 例子中是 chil1 Vnode 对象,源码中对应 getFirstComponentChild 函数
  function getFirstComponentChild (children) {if (Array.isArray(children)) {for (var i = 0; i < children.length; i++) {var c = children[i];
        // 组件实例存在,则返回,实践上返回第一个组件 vnode
        if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {return c}
      }
    }
  }
    1. 判断组件满足缓存的匹配条件,在 keep-alive 组件的应用过程中,Vue源码容许咱们是用 include, exclude 来定义匹配条件,include规定了只有名称匹配的组件才会被缓存,exclude规定了任何名称匹配的组件都不会被缓存。更者,咱们能够应用 max 来限度能够缓存多少匹配实例,而为什么要做数量的限度呢?咱们后文会提到。

拿到子组件的实例后,咱们须要先进行是否满足匹配条件的判断,其中匹配的规定容许应用数组,字符串,正则的模式。

var include = ref.include;
var exclude = ref.exclude;
// 通过判断子组件是否满足缓存匹配
if (
    // not included
    (include && (!name || !matches(include, name))) ||
    // excluded
    (exclude && name && matches(exclude, name))
) {return vnode}

// matches
function matches (pattern, name) {// 容许应用数组['child1', 'child2']
    if (Array.isArray(pattern)) {return pattern.indexOf(name) > -1
    } else if (typeof pattern === 'string') {
        // 容许应用字符串 child1,child2
        return pattern.split(',').indexOf(name) > -1
    } else if (isRegExp(pattern)) {// 容许应用正则 /^child{1,2}$/g
        return pattern.test(name)
    }
    /* istanbul ignore next */
    return false
}

如果组件不满足缓存的要求,则间接返回组件的vnode, 不做任何解决, 此时组件会进入失常的挂载环节。

    1. render函数执行的要害一步是缓存 vnode, 因为是第一次执行render 函数,选项中的 cachekeys数据都没有值,其中 cache 是一个空对象,咱们将用它来缓存 {name: vnode} 枚举,而 keys 咱们用来缓存组件名。因而咱们在第一次渲染 keep-alive 时,会将须要渲染的子组件 vnode 进行缓存。
    cache[key] = vnode;
    keys.push(key);
    1. 将曾经缓存的 vnode 打上标记, 并将子组件的 Vnode 返回。vnode.data.keepAlive = true
13.3.4 实在节点的保留

咱们再回到 createComponent 的逻辑,之前提到 createComponent 会先执行 keep-alive 组件的初始化流程,也包含了子组件的挂载。并且咱们通过 componentInstance 拿到了 keep-alive 组件的实例,而接下来 重要的一步是将实在的 dom 保留再 vnode

function createComponent(vnode, insertedVnodeQueue) {
    ···
    if (isDef(vnode.componentInstance)) {
        // 其中一个作用是保留实在 dom 到 vnode 中
        initComponent(vnode, insertedVnodeQueue);
        // 将实在节点增加到父节点中
        insert(parentElm, vnode.elm, refElm);
        if (isTrue(isReactivated)) {reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
        }
        return true
    }
}

insert的源码不列举进去,它只是简略的调用操作 domapi, 将子节点插入到父节点中,咱们能够重点看看 initComponent 关键步骤的逻辑。

function initComponent() {
    ···
    // vnode 保留实在节点
    vnode.elm = vnode.componentInstance.$el;
    ···
}

因而,咱们很清晰的回到之前遗留下来的问题,为什么 keep-alive 须要一个 max 来限度缓存组件的数量。起因就是 keep-alive 缓存的组件数据除了包含 vnode 这一形容对象外,还保留着实在的 dom 节点, 而咱们晓得实在节点对象是宏大的,所以大量保留缓存组件是消耗性能的。因而咱们须要严格控制缓存的组件数量,而在缓存策略上也须要做优化,这点咱们在下一篇文章也持续提到。

因为 isReactivatedfalse,reactivateComponent函数也不会执行。至此 keep-alive 的首次渲染流程剖析结束。

如果疏忽步骤的剖析,只对首次渲染流程做一个总结:内置的 keep-alive 组件,让子组件在第一次渲染的时候将 vnode 和实在的 elm 进行了缓存。

13.4 形象组件

这一节的最初顺便提一下上文提到的形象组件的概念。Vue提供的内置组件都有一个形容组件类型的选项,这个选项就是{astract: true}, 它表明了该组件是形象组件。什么是形象组件,为什么要有这一类型的区别呢?我感觉归根究底有两个方面的起因。

    1. 形象组件没有实在的节点,它在组件渲染阶段不会去解析渲染成实在的 dom 节点,而只是作为两头的数据过渡层解决,在 keep-alive 中是对组件缓存的解决。
    1. 在咱们介绍组件初始化的时候已经说到父子组件会显式的建设一层关系,这层关系奠定了父子组件之间通信的根底。咱们能够再次回顾一下 initLifecycle 的代码。
Vue.prototype._init = function() {
    ···
    var vm = this;
    initLifecycle(vm)
}

function initLifecycle (vm) {
    var options = vm.$options;

    var parent = options.parent;
    if (parent && !options.abstract) {
        // 如果有 abstract 属性,始终往下层寻找,直到不是形象组件
      while (parent.$options.abstract && parent.$parent) {parent = parent.$parent;}
      parent.$children.push(vm);
    }
    ···
  }

子组件在注册阶段会把父实例挂载到本身选项的 parent 属性上,在 initLifecycle 过程中,会反向拿到 parent 上的父组件 vnode, 并为其$children 属性增加该子组件 vnode, 如果在反向找父组件的过程中,父组件领有abstract 属性,即可断定该组件为形象组件,此时利用 parent 的链条往上寻找,直到组件不是形象组件为止。initLifecycle的解决,让每个组件都能找到下层的父组件以及上层的子组件,使得组件之间造成一个严密的关系树。

有了第一次的缓存解决,当第二次渲染组件时,keep-alive又会有哪些魔法的存在呢,之前留下的缓存优化又是什么?这些都会在下一大节一一解开。

退出移动版