keep-alive的实现原理(vue.js源码分析)

一、写在前面

本内容是基于vue2的源码分析,vue3与vue2原理一致。以下是我自己看源码的理解与笔记,若有误,请指出,谢谢。

二、keepAlive源码及分析

创建组件时,以下代码执行顺序依次是:create → render → mounted

 var KeepAlive = {
    name: "keep-alive",
     /**
     *  abstract属性为true,则表示该组件是一个抽象组件,
     *  它自身不会渲染一个DOM元素,也不会出现在父组件链中,
     * 	执行逻辑在下面的initLifecycle函数中
     */
    abstract: true, 
    // props中的属性就是我们使用keepAlive时传的
    props: {
      include: patternTypes,
      exclude: patternTypes,
      max: [String, Number],
    },
    created: function () {
      // 在该keepAlive组件上添加一个空的cache属性和空的keys属性,用来存后面的缓存组件
      this.cache = Object.create(null);
      this.keys = [];
    },
    
    render: function () {
      var slot = this.$slots.default; // 获取keepAlive包裹的内容(slot)
      var vnode = getFirstComponentChild(slot); // 获取slot中的第一个,所以keepAlive里包裹多个组件无效,只取第一个
      var componentOptions = vnode && vnode.componentOptions; // 获取被包裹组件的componentOptions,若这个属性没有值,则该组件没有内容
      
      if (componentOptions) {
       // 如果不是空组件,就执行下面的内容
        // check pattern
        var name_2 = _getComponentName(componentOptions); // 获取被包裹组件(vnode)的name或tag


          /**
         * 如果keepAlive组件的exclude中 包含 vnode的name或tag,
         * 或者keepAlive组件的include中 不包含 vnode的name或tag,
         * 则直接return vnode,不执行后面的代码了
         */
        var _a = this,
          include = _a.include,
          exclude = _a.exclude;
        if (
          // not included
          (include && (!name_2 || !matches(include, name_2))) ||
          // excluded
          (exclude && name_2 && matches(exclude, name_2))
        ) {
          return vnode;
        }


 		/**
         * 如果keepAlive组件的exclude中 不包含 vnode的name或tag,
         * 或者keepAlive组件的include中 包含 vnode的name或tag,
         * 则继续执行下面的代码
         */
        var _b = this,
          cache = _b.cache,
          keys = _b.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 ? "::".concat(componentOptions.tag) : "")
            : vnode.key;   // 按一定规则生成一个key作为被包裹组件vnode的标识

		
        if (cache[key]) {
        /** 
        *	cache就是created中定义的,如果vnode的key之前缓存过,则执行下面的;
        *   vnode第一次进来的时候cache[key]一定不为真,会走else里的逻辑
        */
          vnode.componentInstance = cache[key].componentInstance;

          // make current key freshest
          remove$2(keys, key);
          keys.push(key);
        } else {
        /** 
        *	vnode第一次进来时会进入此处的逻辑   
        */
          // delay setting the cache until update
          this.vnodeToCache = vnode;
          this.keyToCache = key;
        }
        // @ts-expect-error can vnode.data can be undefined
        vnode.data.keepAlive = true; // 需要被缓存的组件的vnode打上一个标记
      }
      return vnode || (slot && slot[0]); // 如果vnode是空组件,或者vnode不是空组件且是需要缓存的,则return vnode或者keepAlive中的第一个slot
    },

     mounted: function () {
      var _this = this;
      this.cacheVNode(); // 此方法具体逻辑见下面methods中
      // 监听黑白名单(exclude、include)中的变动,实时更新this.cache对象
      this.$watch("include", function (val) {
        pruneCache(_this, function (name) {
          return matches(val, name);
        });
      });
      this.$watch("exclude", function (val) {
        pruneCache(_this, function (name) {
          return !matches(val, name);
        });
      });
    },
    
    updated: function () {
      this.cacheVNode();
    },
    
    destroyed: function () {
    // keepAlive组件的destroyed生命周期执行时,会遍历缓存里的每个组件,删除缓存并销毁它们
      for (var key in this.cache) {
        pruneCacheEntry(this.cache, key, this.keys);
      }
    },
    methods: {
      cacheVNode: function () {
        var _a = this,
          cache = _a.cache,
          keys = _a.keys,
          // 从上面的render()中可以知道,this.vnodeToCache只有当被包裹组件(vnode)是第一次加载时,才会存在
          vnodeToCache = _a.vnodeToCache,
          keyToCache = _a.keyToCache;
        if (vnodeToCache) {
          var tag = vnodeToCache.tag,
            componentInstance = vnodeToCache.componentInstance,
            componentOptions = vnodeToCache.componentOptions;
            // 第一次加载vnode时,将它存到keepAlive的cache属性中
          cache[keyToCache] = {
            name: _getComponentName(componentOptions),
            tag: tag,
            componentInstance: componentInstance,
          };
          keys.push(keyToCache); // 把生成的vnode的key添加到keepAlive的keys中
          // 如果缓存超过设置的最的的数量了,则删除最先存进来的一个
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode);
          }
          this.vnodeToCache = null; // 清空vnodeToCache
        }
      },
    },    
  };

  // 删除cache对象数据函数
   function pruneCache(keepAliveInstance, filter) {
    var cache = keepAliveInstance.cache,
      keys = keepAliveInstance.keys,
      _vnode = keepAliveInstance._vnode;
    for (var key in cache) {
      var entry = cache[key];
      if (entry) {
        var name_1 = entry.name;
        if (name_1 && !filter(name_1)) {
          pruneCacheEntry(cache, key, keys, _vnode);
        }
      }
    }
  }
  function pruneCacheEntry(cache, key, keys, current) {
    var entry = cache[key];
    if (entry && (!current || entry.tag !== current.tag)) {
      // @ts-expect-error can be undefined
      entry.componentInstance.$destroy(); //销毁组件
    }
    cache[key] = null;
    remove$2(keys, key);
  }


// vue组件的初始化生命周期函数
 function initLifecycle(vm) {
    var options = vm.$options;
    // 定位第一个非abstract父组件实例
    var parent = options.parent;
    if (parent && !options.abstract) {
      // 如果不是抽象组件
      while (parent.$options.abstract && parent.$parent) { 
        parent = parent.$parent;
      }
      // 如果是抽象组件
      parent.$children.push(vm);
    }
    vm.$parent = parent;
    vm.$root = parent ? parent.$root : vm;
    vm.$children = [];
    vm.$refs = {};
    vm._provided = parent ? parent._provided : Object.create(null);
    vm._watcher = null;
    vm._inactive = null;
    vm._directInactive = false;
    vm._isMounted = false;
    vm._isDestroyed = false;
    vm._isBeingDestroyed = false;
  }

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

Vue的渲染是从render阶段开始的,但keep-alive的渲染是在patch阶段,这是构建组件树(虚拟DOM树),并将VNode转换成真正DOM节点的过程。
在patch阶段,会执行createComponent函数:

function createComponent(vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      // isDef函数用于判断i是否存在,当存在时返回true
      if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive; // 是否缓存过
        if (isDef((i = i.hook)) && isDef((i = i.init))) {
          i(vnode, false /* hydrating */); // 如果没有被缓存过,则代码执行到这里就结束
        }
        /**
          * 调用init钩子后,如果vnode是一个子组件,
          * 它应该创建一个子实例并挂载它,
          * 子组件还设置了占位符vnode的elm.
          * 在这种情况下,我们可以返回元素并完成。
		*/
        if (isDef(vnode.componentInstance)) {
        /**
        * keepAlive组件的render函数中可以看到,vnode.componentInstance是在被包裹组件
        * 在非首次加载时赋值的,所以当非首次加载时,就直接往父元素中插入缓存中的dom
        */
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm);
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true;
        }
      }
    }

四、keep-alive包裹的组件为什么只执行一次Mounted、为什么没有被销毁

 // patch期间在组件VNode上调用的内联挂钩
  var componentVNodeHooks = {
  // 初始化组件钩子函数
    init: function (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); // 缓存过的组件不再走$mount,那么mounted之前的所有钩子函数(beforeCreate、created、mounted)都不再执行。
      } else {
        var child = (vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        ));
        child.$mount(hydrating ? vnode.elm : undefined, hydrating); // 没有被缓存过的组件会调用$mount函数
      }
    },
    prepatch: function (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var child = (vnode.componentInstance = oldVnode.componentInstance);

      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },
    insert: function (vnode) {
      var context = vnode.context,
        componentInstance = vnode.componentInstance;

      if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook$1(componentInstance, "mounted");
      }
      if (vnode.data.keepAlive) {
        if (context._isMounted) {
          /**
          *  在更新过程中,保持活动的组件的子组件可能会发生变化,因此直接在这里遍历树可能会在不正确的子组件上调用激活的钩子。
          * 相反,我们将它们推入队列,在整个patch过程结束后进行处理
          */
          queueActivatedComponent(componentInstance);
        } else {
        // activateChildComponent函数递归地去执行所有子组件的activated钩子函数
          activateChildComponent(componentInstance, true /* direct */);
        }
      }
    },
    destroy: function (vnode) {
      var componentInstance = vnode.componentInstance;
      if (!componentInstance._isDestroyed) {
        if (!vnode.data.keepAlive) {
        // 没有被缓存的组件则销毁
          componentInstance.$destroy();
        } else {
          deactivateChildComponent(componentInstance, true /* direct */); // 当是缓存的组件则没有销毁,而是调用deactivateChildComponent()
        }
      }
    },
  };

参考博客:https://blog.csdn.net/weixin_57334089/article/details/121218541

你可能感兴趣的:(vue.js,前端)