精通Vue(13):keep-alive实现原理

内容分享19小时前发布
0 0 0

提示:后面有完整调试代码,先运行看下结果再配合文章。

昨天发过一篇,但感觉写的很不好就删了,又花费了几个小时重新写了一篇,这篇完整的解释了keep-alive的运行流程。

本篇应该是本系列最复杂的一篇,作者尽力通俗的表达清楚。

下面进入正题!

Keep-alive是什么?

keep-alive就是将它肚子包裹的节点进行了缓存,一般情况下是一个组件,代码如下:

<keep-alive>
  <test-component v-if="isShow">
  </test-component>
</keep-alive>

我们第一个要解决的问题就是,<keep-alive>这个标签是什么?通过Vue的内部定义,可以知道它是一个组件。但是一个组件内部又包裹一个组件,这个包裹的节点是什么呢?

renderChildren的概念

可以肯定的是,这个内部的组件不是<keep-alive>组件 的东西,为什么呢?我们可以这样想,如果<keep-alive>需要某个元素,它直接在自己的模板里面写就可以了,没有必要在使用时嵌套一个元素。

所以这个内嵌的组件在Vue中称为renderChildren,在第8篇文章写slot时提到过,renderChildren是被挑选而不是渲染的元素,举个slot的例子容易对比清楚:

<child>
  <h2 slot="title"></h2>
</child>

<child>是一个组件,它内部包裹的<h2>是准备挑选替换自己模板中的<slot>的。再看<keep-alive>元素,类比可知,它就是一个renderChildren。

抽象组件

根据原理,renderChildren是替换slot的,那么keep-alive有这个slot吗?它没有,而且什么都没有,也就是说keep-alive是一个抽象组件,它一片DOM都没有。那么接着就会出现这个问题,当渲染到它时,它怎么处理呢?下面是一段二人对话:

Vue:<keep-alive>目前要渲染到你了,快把你的Vnode拿出来。
keep-alive:我是抽象组件,什么DOM都没有,到哪里生成Vnode。
Vue:那怎么办呢?
Keep-alive:我虽然没有,但是我有一个儿子也就是renderChildren,我把它的Vnode给你,就代表渲染我了。
Vue:没问题。我们不管是渲染什么,只要你给出一个结果就可以了。

这段对话清楚的表明了这个过程,也就是说<keep-alive>组件虽然不存在DOM,但是一点都不影响它收集自己的儿子renderChildren,然后在render函数中,直接将它的Vnode返回给Vue。

keep-alive的render函数

由于keep-alive没有DOM,所以只能自定义render函数,在这个函数中,它主要做了两件事:

  1. 将自己的renderChildren,也就是本例中的<test-component>组件的Vnode缓存到cache中,这就是缓存的原理。
  2. 将该组件的Vnode返回给系统。

图示如下:

精通Vue(13):keep-alive实现原理

截止到这一步,Vue拿到了渲染<keep-alive>组件的Vnode,它也不管是谁的Vnode,反正有结果就可以了。

Vue渲染过程

在本例中,代码比较简单。Vue开始渲染,在解析v-if时引用了变量isShow,进而产生依赖收集,此时的实例是根实例,那么就将根实例的watcher加入。

<div id="app">
    <button @click="isShow = !isShow">
        修改组件
    </button>
    <keep-alive>
        <test-component v-if="isShow"></test-component>
    </keep-alive>
</div>

接着就是常规的组件渲染,过程如下:

渲染到<keep-alive>组件时,生成组件实例并挂载,在挂载过程中使用了<keep-alive>的render函数,从该函数中拿到renderChildren,也即<test-component>的Vnode,接着在patch中再创建组件,走了一遍组件创建的过程,这些是常规过程,在这里省略。

button的点击事件

button点击之后,isShow变为false,因此引起vm根实例进行全局更新,在render函数的重新计算中<test-component>不存在了,因此返回一个emptyVnode,也就是一个空Vnode。

Vue计算完成Vnode之后,再次进行patch,当前的情形如下:

精通Vue(13):keep-alive实现原理

Vue内部进行diff比较算法,可以看出新旧基本一样,在进行第二个Vnode,也就是<keep-alive>的Vnode比较时,调用patchNode方法。

在这个方法中,有下面一段代码:

.....
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
  i(oldVnode, vnode);
}
.....

意思是:如果存在prepatch的钩子方法,可以先执行一下。在这个方法中又调用function updateChildComponent,在该方法中主要做了下面的判断:

var needsForceUpdate = !!(
    renderChildren ||               // has new static slots
    vm.$options._renderChildren ||  // has old static slots
    hasDynamicScopedSlot
);

是什么意思呢?它主要说了一点,就是如果组件的肚子里面包裹有renderChildren,那么就需要强制更新一下。举个例子,列如:

<child>
  <h2>..</h2>
</child>

分析:chlid组件包含一个renderChildren,那么全局渲染时,就会渲染到该组件,此时就要求<child>必须更新。我觉得它是这么思考的,由于它里面包含一个可能变化的量(一般都是用于替换slot),所以它代表一种不稳定,那么这个renderChildren只要存在就强制更新。

这一点是本篇文章的核心,它解释了一个重大问题:为什么v-if为false且全局更新之后,<test-component>就不见了,我们可以看到,这是一种比较特殊的情况,专门为renderChildren定制的强制更新!

投入异步更新队列

强制更新的结果就是将当前实例,即<keep-alive>组件实例的R-watcher投入异步更新队列。那么就会再次执行<keep-alive>的render函数,但由于v-if条件为false,此时返回一个空Vnode,最后进入patch函数。

旧Vnode是一个组件实例,新Vnod是一个空Vnode,先将新元素插入,由于是空Vnode等于没做。接着删除旧Vnode。

精通Vue(13):keep-alive实现原理

旧Vnode下架

removeVnodesremoveAndInvokeRemoveHook(ch);
invokeDestroyHook(ch);

旧Vnode下架就是指将<test-component>对应的DOM卸载。

removeAndInvokeRemoveHook(ch)方法主要做的事情就是将DOM元素删除,而invokeDestroyHook则做最后的善后清理工作。

invokeDestroyHook:最后的处理工作

这个方法主要调用了组件的destory钩子函数,代码如下:

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

分析:由于它是组件而且具有keepAlive性质(被keep-alive包裹),因此不是直接销毁组件,而是卸载组件,那么就执行下面的deactivateChildComponent方法。

deactivateChildComponent方法

这个方法也做了一些事情,主要就是执行deactivated钩子方法,以此说明组件被卸载了(不是销毁了)。

重新显示时

重新显示时,全局进行渲染,此时<keep-alive>又有renderChildren,进行转化为$slot,那么就从缓存中直接取出Vnode,由于Vnode还绑定了组件实例,因此该组件的整个状态包括数据都存在。

流程结束!

总结

这篇文章写了keep-alive的整个流程,它涉及到Vue大部分知识点,四个字:波澜壮阔!很少有JS代码像这几个前端框架写的这么复杂和晦涩,我觉得主要是它的架构超级庞大,而且太多细节要处理的缘由!!

下面做个简洁的总结:

  1. keep-alive不存在DOM,而是将自己的renderChildren的Vnode拿出来渲染。
  2. keep-alive会将renderChildren的Vnode进行缓存,而且Vnode绑定组件实例,所以数据和状态齐全。
  3. 如果组件嵌套有renderChildren,不管怎样,它必定会重新渲染,主要是思考这个东西易变化。
  4. 组件下架时,不是进行销毁,而是进行拆卸。由于它是在keep-alive内部包裹的。

完整的调试代码

<div id="app">
    <button @click="isShow = !isShow">
        {{isShow ? '隐藏组件' : '显示组件'}}
    </button>
    <!-- 查看状态 -->
    <div style="color:red">{{info}}</div>

    <keep-alive>
        <!-- 绑定两个hook,从父组件的角度监控这个组件 -->
        <test-component v-if="isShow" @hook:activated="activated" @hook:deactivated="deactivated"></test-component>
    </keep-alive>
</div>
<script src="../vue.js"></script>
<script>
    Vue.component('testComponent', {
        template: `
                <div>
                    <p>我是测试组件</p>
                    <input placeholder="输入内容后隐藏再显示试试" />
                </div>
            `
    });

    new Vue({
        el: '#app',
        data() {
            return {
                isShow: true, // 控制组件显示/隐藏的状态
                info: '',
            }
        },
        methods: {
            activated() {
                this.info = '组件激活了!!'
            },
            deactivated() {
                this.info = '组件卸载了!!'
            }
        }
    });
</script>
© 版权声明

相关文章

暂无评论

none
暂无评论...