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


    当使用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>

    渲染结果为:

    控制台输出:

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

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

    渲染出的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里
      }

    writer by:大沙漠 QQ:22969969

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

  • 相关阅读:
    数据库范式
    SQL基础
    JAVA流
    response.getWriter()和jsp中的out对象的区别
    JAVA排序(冒泡,直接选择,反转)
    Collections类常用方法
    JAVA集合
    JAVA面向对象(重载,重写 常用的 toString/equals)
    Java面向对象一(封装 继承 多态 类 对象 方法)
    JAVA基础
  • 原文地址:https://www.cnblogs.com/greatdesert/p/11287509.html
Copyright © 2020-2023  润新知