欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

程序员文章站 2022-07-29 23:00:30
当使用is特性切换不同的组件时,每次都会重新生成组件Vue实例并生成对应的VNode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如: 渲染结果为: 控制台输出: 当我们在输入框输入内容后再点击切换将切换到B组件后控制台输出: 然后再次点击切换,将显示A组件,此时控制台输出: ......

当使用is特性切换不同的组件时,每次都会重新生成组件vue实例并生成对应的vnode进行渲染,这样是比较花费性能的,而且切换重新显示时数据又会初始化,例如:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>

    <div id="app">
        <button @click="currentcomp=currentcomp=='a'?'b':'a'">切换</button>               <!--动态组件-->
        <component :is="currentcomp"/>
    </div>
    <script>
        with(vue.config){productiontip=false;devtools=false;}
        var app  = new vue({
            el: '#app',
            components:{
                a:{
                    template:"<div><input type='text'/></div>",
                    name:'a',
                    mounted:function(){console.log('comp a mounted');}
                },
                b:{
                    template:"<div>b组件</div>",
                    name:'b',
                    mounted:function(){console.log('comp b mounted');}
                }
            },
            data:{
                currentcomp:"a"
            }
        })
    </script>

</body>
</html>

渲染结果为:

Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

控制台输出:

Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

当我们在输入框输入内容后再点击切换将切换到b组件后控制台输出:

Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

然后再次点击切换,将显示a组件,此时控制台输出:

Vue.js 源码分析(三十一) 高级应用 keep-alive 组件 详解

渲染出的a组件内容是空白的,我们之前在输入框输入的内容将没有了,这是因为使用is特性切换不同的组件时,每次都会重新生成组件vue实例并生成对应的vnode进行渲染,数据会丢失的

解决办法是可以用kepp-alive组件对子组件内的组件实例进行缓存,子组件激活时将不会再创建一个组件实例,而是从缓存里拿到组件实例,直接挂载即可,

使用keep-alive组件时,可以给该组件传递以下特性:
    include         ;只有名称匹配的组件会被缓存        ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
    exclude         ;任何名称匹配的组件都不会被缓存        ;只可以是字符串数组、字符串(以逗号分隔,分隔后每个内容就是要缓存的组件名)、正则表达式
    max            ;数字。最多可以缓存多少组件实例

keep-alive对应的子组件有两个生命周期函数,这两个生命周期是keep-alive特有的,如下:
    activated        ;该子组件被激活时调用
    deactivated         ;该子组件被停用时调用

例如:

<!doctype html>
<html lang="en">
<head>
    <meta charset="utf-8">
    <title>document</title>
    <script src="https://cdn.jsdelivr.net/npm/vue@2.5.16/dist/vue.js"></script>
</head>
<body>
    <div id="app">
        <button @click="currentcomp=currentcomp=='a'?'b':'a'">切换</button>        
        <keep-alive>
            <component :is="currentcomp"/>
        </keep-alive>
    </div>
    <script>
        with(vue.config){productiontip=false;devtools=false;}
        var app  = new vue({
            el: '#app',
            components:{
                a:{
                    template:"<div><input type='text'/></div>",
                    name:'a',
                    mounted:function(){console.log('comp a mounted');},                 //挂载事件
                    activated:function(){console.log("comp a activated");},             //激活时的事件,kepp-alive独有的生命周期函数
                    deactivated:function(){console.log("comp a deactivated");}          //停用时的事件,kepp-alive独有的生命周期函数
                },
                b:{
                    template:"<div>b组件</div>",
                    name:'b',
                    mounted:function(){console.log('comp b mounted');},
                    activated:function(){console.log("comp b activated");},
                    deactivated:function(){console.log("comp b deactivated");}
                }
            },
            data:{
                currentcomp:"a"
            }
        })
    </script>
</body>
</html>

这样组件在切换时之前的数据就不会丢失了。

 

源码分析


 对于keep-alive来说,是通过initglobalapi()函数注册的,如下:

var builtincomponents = {           //第5059行,keppalive组件的定义
  keepalive: keepalive
}
function initglobalapi (vue) { //第5015行 /**/ extend(vue.options.components, builtincomponents); //第5051行 /**/ }

keep-alive组件的定义如下:

var keepalive = {         //第4928行
  name: 'keep-alive',
  abstract: true,

  props: {
    include: patterntypes,
    exclude: patterntypes,
    max: [string, number]
  },

  created: function created () {            //创建时的周期函数
    this.cache = object.create(null);         //用于缓存keepalive的vnode
    this.keys = [];                           //设置this.keys为空数组 
  },

  destroyed: function destroyed () {      //销毁生命周期
    var this$1 = this;

    for (var key in this$1.cache) {
      prunecacheentry(this$1.cache, key, this$1.keys);
    }
  },

  mounted: function mounted () {          //挂载时的生命周期函数
    var this$1 = this;

    this.$watch('include', function (val) {                         //监视include的变化
      prunecache(this$1, function (name) { return matches(val, name); });
    });
    this.$watch('exclude', function (val) {                         //监视exclude的变化
      prunecache(this$1, function (name) { return !matches(val, name); });
    });
  },

  render: function render () {          //render函数
    /**/
  }
}

keep-alive也是一个抽象组件(abstract属性为true),mounted挂载时会监视include和exclude的变化,也就是说程序运行时可以通过修改include或exclude来对keep-alive里缓存的子组件进行移除操作。

keep-alive组件的render函数如下:

  render: function render () {          //第4926行 keepalive组件的render函数
    var slot = this.$slots.default;                             //获取所有的子节点,是个vnode数组
    var vnode = getfirstcomponentchild(slot);                   //拿到第一个组件vnode
    var componentoptions = vnode && vnode.componentoptions;     //该组件的配置信息
    if (componentoptions) {
      // check pattern
      var name = getcomponentname(componentoptions);              //获取组件名称,优先获取name属性,如果没有则获取tag名称
      var ref = this;                                             //当前keppalive组件的vue实例
      var include = ref.include;                                  //获取include属性
      var exclude = ref.exclude;                                  //获取exclude属性
      if (
        // not included
        (include && (!name || !matches(include, name))) ||
        // excluded
        (exclude && name && matches(exclude, name))               //执行matches进行匹配,如果该组件不满足条件
      ) { 
        return vnode                                                //则直接返回vnode,即不做处理
      }

      var ref$1 = this;
      var cache = ref$1.cache;
      var keys = ref$1.keys;
      var key = vnode.key == null
        // same constructor may get registered as different local components
        // so cid alone is not enough (#3269)
        ? componentoptions.ctor.cid + (componentoptions.tag ? ("::" + (componentoptions.tag)) : '')
        : vnode.key;                                              //为子组件定义一个唯一的key值 如果该子组件没有定义key则拼凑一个,值为该组件对应的vue实例的cid::tag,例如:1::a  同一个构造函数可以注册为不同的组件,所以单凭一个cid作为凭证是不够的
      if (cache[key]) {                                           //如果该组件被缓存了
        vnode.componentinstance = cache[key].componentinstance;     //直接将该组件的实例保存到vnode.componentinstance里面
        // make current key freshest    
        remove(keys, key);                                          
        keys.push(key);
      } else {                                                     //如果当前组件没有被缓存
        cache[key] = vnode;                                             //先将vnode保存到缓存cache里
        keys.push(key);                                                 //然后将key保存到keys里
        // prune oldest entry
        if (this.max && keys.length > parseint(this.max)) {             //如果指定了max且当前的keys里存储的长度大于this.max
          prunecacheentry(cache, keys[0], keys, this._vnode);             //则移除keys[0],这是最不常用的子组件
        }
      }

      vnode.data.keepalive = true;                                //设置vnode.data.keepalive为true,即设置一个标记
    }
    return vnode || (slot && slot[0])                             //最后返回vnode(即第一个组件子节点)
  }

matches用于匹配传给kepp-alive的include或exclude特性是否匹配,如下:

function matches (pattern, name) {    //第4885行      //查看name这个组件是否匹配pattern
  if (array.isarray(pattern)) {                        //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)
  }
  /* istanbul ignore next */
  return false
}

初次渲染时,keep-alive下的组件和普通组件是没有区别的,当一个组件从被激活变为激活状态时,和keep-alive相关的逻辑如下:

执行patch()将vnode渲染成真实节点时会执行createelm()函数,又会优先执行createcomponent创建组件实例,如下:

  function createcomponent (vnode, insertedvnodequeue, parentelm, refelm) {   //第5589行 创建组件节点
    var i = vnode.data;                                                         //获取vnode的data属性
    if (isdef(i)) {                                                             //如果存在data属性(组件vnode肯定存在这个属性,普通vnode有可能存在)
      var isreactivated = isdef(vnode.componentinstance) && i.keepalive;          //是否为激活操作 如果vnode.componentinstance为true(组件实例存在)且存在keepalive属性则表示为keepalive组件
      if (isdef(i = i.hook) && isdef(i = i.init)) {                              
        i(vnode, false /* hydrating */, parentelm, refelm);                         //执行组件的init钩子函数
      }
      // after calling the init hook, if the vnode is a child component
      // it should've created a child instance and mounted it. the child
      // component also has set the placeholder vnode's elm.
      // in that case we can just return the element and be done.
      if (isdef(vnode.componentinstance)) {
        initcomponent(vnode, insertedvnodequeue);                               //将子组件的vnode push到insertedvnodequeue里面,
        if (istrue(isreactivated)) {                                            //如果是keep-alive激活的状态
          reactivatecomponent(vnode, insertedvnodequeue, parentelm, refelm);        //执行reactivatecomponent()函数
        }
        return true
      }
    }
  }

init是组件的钩子函数,用于创建组件的实例,如下:

  init: function init (           //第4109行 组件的安装
    vnode,
    hydrating,
    parentelm,
    refelm
  ) {
    if (
      vnode.componentinstance &&
      !vnode.componentinstance._isdestroyed &&
      vnode.data.keepalive
    ) {                                                             //如果vnode.componentinstance和vnode.data.keepalive都存在,则表示是一个keep-alive组件的激活状态
      // kept-alive components, treat as a patch
      var mountednode = vnode; // work around flow
      componentvnodehooks.prepatch(mountednode, mountednode);       //执行该组件的prepatch方法
    } else {
      var child = vnode.componentinstance = createcomponentinstanceforvnode(
        vnode,
        activeinstance,
        parentelm,
        refelm
      );
      child.$mount(hydrating ? vnode.elm : undefined, hydrating);
    }
  },

对于keep-alive子组件的激活过程来说,它是不会调用createcomponentinstanceforvnode去创建一个新的组件实例的,而是直接从vnode的componentinstance拿到组件实例即可

回到createcomponent()函数,最后会执行reactivatecomponent()函数,该函数就比较简单了,就是将子组件vnode.elm插入到dom中,如下:

  function reactivatecomponent (vnode, insertedvnodequeue, parentelm, refelm) {     //第5628行  激活一个组件
    var 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.
    var 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);                                             //调用insert将vnode.elm插入到parentelm里
  }

insert会调用原生的insertbefore或者appendchild这去插入dom,最后返回到patch()函数内,就把之前的b组件从dom树中移除,并执行相关生命周期函数。