Vue之keep-alive细节

前言

keep-alive组件是Vue的内部组件,Vue官方对于keep-alive的描述是:

keep-alive包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们。keep-alive是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在组件的父组件链中。当组件在keep-alive内被切换,它的 activated 和 deactivated 这两个生命周期钩子函数将会被对应执行

从上面的描述可以得到如下几点信息:

  • keep-alive是抽象组件自身不会渲染到页面,也不会出现在父组件链中
  • keep-alive的缓存针对的是动态组件
  • 组件切换回触发2个生命周期函数activated、deactivated

keep-alive是抽象组件

keep-alive是抽象组件,其特点有:

  • 自身不会渲染一个DOM元素
  • 不会出现在组件的父组件链中

keep-alive是vue内部组件,具体组件定义为:

var KeepAlive = {
     
	name: 'keep-alive',
    abstract: true,
    props: {
     
    	include: patternTypes,
        exclude: patternTypes,
        max: [String, Number]
    },
    created: function created () {
     
        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;
        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); });
        });
     },
     render: function render () {
     
     	// 相关代码
      }
    };
特点1:自身不会渲染为一个DOM元素

Vue组件创建虚拟节点VNode对象是通过render函数来实现,相关逻辑:

vm._update(vm._render(), hydrating);

vm的_render函数就是执行render函数返回对应的vnode,而keep-alive组件不会渲染为一个DOM元素,核心的思想在于render函数最后返回的vnode对象。

render() {
     
	// 核心代码
	var slot = this.$slots.default;
	var vnode = getFirstComponentChild(slot);
	// 其他逻辑
	return vnode || (slot && slot[0])
}

上面代码是内部组件keep-alive其render函数的一些逻辑,从上面代码逻辑中可以知道keep-alive返回虚拟节点是其子组件vnode或子标签vnode。

特点2:不会出现在组件的父组件链中

这部分逻辑是在Vue初始化过程中initLifecycle函数的中,核心代码如下:

var parent = options.parent;
if (parent && !options.abstract) {
     
	while (parent.$options.abstract && parent.$parent) {
     
		parent = parent.$parent;
    }
    parent.$children.push(vm);
}

可以看出组件options.abstract是false才会将组件实例对象放置到$children中,这是为什么keep-alive组件本身不会出现在组件的父组件链中的原因。

keep-alive针对组件缓存

实际上这个特点结合keep-alive组件自身的render函数相关逻辑来看,具体逻辑如下:

render() {
     
	var vnode = getFirstComponentChild(slot);
    var componentOptions = vnode && vnode.componentOptions;
    if (componentOptions) {
     
    	// 相关缓存逻辑
    }
}

这里缓存逻辑的关键在于componentOptions是否存在,而componentOptions表示是组件的配置文件对象,而Vue中创建VNode对象传入该属性的处理只有一处即根节点render函数的执行处理createComponent中调用new VNode传入。

根节点render函数处理只有组件才会调用createComponent来处理,所以对于原生HTML标签或SVG标签是不会存在缓存处理的

这里需要说明的是keep-alive针对动态组件缓存,这里的动态组件严格来说就是组件而已,而keep-alive针对动态组件缓存这个说法更准确来说是:

在每次渲染过程中,keep-alive会缓存其包括的第一个需要显示出来的组件

渲染出来的组件只会是keep-alive组件的第一个,如果存在多个需要显示的组件也只会显示第一个。例如:

<keep-alive>
	<component1>测试1component1>
	<component2>测试2component2>
keep-alive>

上面的代码只会显示出现测试1内容,而测试2内容是不会显示出现的,这都是源于keepa-alive中getFirstComponentChild的逻辑处理。

keep-alive缓存的实现

实际上还是需要结合keep-alive的render方法逻辑来看,实际逻辑是清晰的,具体逻辑如下:

if (cache[key]) {
     
	vnode.componentInstance = cache[key].componentInstance;
    // 移除对应key,更新当前key的新鲜度
    remove(keys, key);
    keys.push(key);
} else {
     
	cache[key] = vnode;
    keys.push(key);
    // 当超过缓存上限时,移除最老的key
    if (this.max && keys.length > parseInt(this.max)) {
     
    	pruneCacheEntry(cache, keys[0], keys, this._vnode);
    }
}

实际上keep-alive缓存的实现就是通过cache对象 + keys标识新鲜度来实现的,逻辑还是很清晰的。

activated、deactivated生命周期触发

从Vue整体构建过程中可知(可通过Vue内部组件 这篇文章了解)Vue中组件对应的虚拟节点都存在一系列的hook钩子函数,其中insert和destroy是该部分内容需要关注的点。

activated生命周期触发

Vue pacth阶段会调用patch函数来处理相关逻辑,其中必然会执行invokeInsertHook逻辑,即调用虚拟节点的insert钩子函数,该钩子函数重要逻辑如下:

insert: function insert(vnode) {
     
	// 其他逻辑
	if (vnode.data.keepAlive) {
     
		if (context._isMounted) {
     
	    	// vue-router#1212
	        // During updates, a kept-alive component's child components may
	        // change, so directly walking the tree here may call activated hooks
	        // on incorrect children. Instead we push them into a queue which will
	        // be processed after the whole patch process ended.
	        queueActivatedComponent(componentInstance);
	     } else {
     
	        activateChildComponent(componentInstance, true /* direct */);
	     }
	}
}

通过上面逻辑可知对于keepAlive组件会调用相关activateChildComponent(这里先暂时不管queueActivatedComponent的逻辑),而activateChildComponent函数主要是两个逻辑:

  • 递归处理子组件即调用activateChildComponent自身(keep-alive下的所有子组件的activated都会触发)
  • callHook(vm, ‘activated’)即执行activated生命周期函数
deactivated生命周期触发

同activated生命周期触发,在Vue patch阶段会对旧节点执行invokeDestroyHook操作,即调用虚拟节点的destroy钩子函数,该钩子函数重要逻辑如下:

destroy: function destroy (vnode) {
     
	var componentInstance = vnode.componentInstance;
    if (!componentInstance._isDestroyed) {
     
    	if (!vnode.data.keepAlive) {
     
    		componentInstance.$destroy();
        } else {
     
          	deactivateChildComponent(componentInstance, true /* direct */);
        }
     }
}

deactivateChildComponent函数的逻辑跟activateChildComponent函数类似,主要是两个逻辑:

  • 递归处理子组件即调用deactivateChildComponent自身(keep-alive下的所有子组件的deactivated都会触发)
  • callHook(vm, ‘deactivated’)即执行activated生命周期函数

keep-alive与transition组件同用

实际上这2个组件同用,无非就是keep-alive包裹transition或者transition包裹keep-alive这2种情况。这里分析keep-alive包裹transition这种情况,通过这个情况分析来分析最优效果。实际上keep-alive包裹transition这种情况动画效果是生效的,而不是说不没法工作的。
通过上面对keep-alive的逻辑分析,可知keep-alive总会获取第一个存在满足条件的组件,而这个条件简单来说就是存在componentOptions值(而实际上普通自定义组件都存在该值)。而keep-alive这个逻辑会导致缓存transition这个组件本身,而对于transition组件包括的子组件是不起效果的。

当需要用到keep-alive + transition肯定是希望尽可能多缓存组件,而keep-alive包裹transition这种情况没法缓存实际参入动画的组件,仅仅是缓存了transition组件本身

从源码来看,实际应用场景还是通过transition包裹keep-alive来处理动画组件的缓存,实际上Vue官网有一处例子:


<transition>
  <keep-alive>
    <component :is="view">component>
  keep-alive>
transition>

之后会开另一篇文章聊聊transition,从源码分析该内部组件的具体逻辑。

你可能感兴趣的:(Vue相关,vue,keep-alive)