vue 2.0 路由切换以及组件缓存源代码重点难点分析

Posted pzhu1

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue 2.0 路由切换以及组件缓存源代码重点难点分析相关的知识,希望对你有一定的参考价值。

关于vue 2.0源代码分析,已经有不少文档分析功能代码段比如watcher,history,vnode等,但没有一个是分析重点难点的,没有一个是分析大命题的,比如执行router.push之后到底是如何执行代码实现路由切换的?

本文旨在分享本人研究vue 2.0源代码重点难点之结果,不涉及每段源代码具体分析,源代码功能段每个人都可以去分析,只要有耐心,再参考已有高手发表的源代码分析文档,不是太难,主要是要克服一些编程技术问题,比如嵌套回调,递归,对象/数组特殊处理方法等等。

 希望本文对那些有兴趣研究vue源代码但遇到困难无解的网友有帮助,研究源代码是个人兴趣个人的事情,需要自己去debug跟踪研究,只是看别人的研究文档并不代表自己真的懂多少,有多少提高,所以

本文不涉及从头到尾每段源代码的具体的分析,那个自己有兴趣去看看研究研究即可,不是太难,只要对js对象编程技术有一定理解就可以。

 

首先要说的是,vue 2.0的复杂性和难点都是由于采用vnode技术引起的,如果不采用vnode技术,像1.0那样,就没有这些复杂性和难点。

顺带提一下vue 1.0,Vue 1.0最大的迷惑是组件数据变化时如何处触发页面更新?

 

答案在:

       var watcher = new Watcher(vm, expOrFn, cb, options);

 

以及:

function defineReactive(obj,key,val,customSetter) {

       var dep = new Dep(); //每个属性建立一套dep,会复制/引用保存到set/get方法中与属性一起存在

       Object.defineProperty(obj, key, {

              get: function reactiveGetter () {  //创建watcher时会访问执行属性的get方法获取表达式的值!!!

                     if (Dep.target) { //当前正在创建的watcher实例保存在全局!!!

                            dep.depend(); //把当前正在创建的watcher实例保存到属性的dep中

              set: function reactiveSetter (newVal) {

                     dep.notify(); //去属性的dep找watcher/update执行更新页面中绑定的指令表达式

 

顺带,vuex是用computed方法实现的,而computed方法是基于defineReactive实现的,就是defineReactive技术。

 

vue 1.0具体就不再分析,网上已经有几个文档分析很透彻。

 

2.0从router.push()开始路由切换时执行transitionTo/confirmtransition的代码莫名其妙,似乎不太对劲,到底最关键的代码逻辑流程在哪里?确实很难破解,因为涉及到源代码总体关键设计思想逻辑,甚至可以说是

设计奥秘,vue作者是个了不起的大神,大神的代码都有很隐蔽很深奥的设计逻辑和编程代码难以破解,比如angular,它的模块机制非常复杂深奥,源代码难以破解。

 

本文命题破解要点:

1)每个组件都会创建new watcher:

vm._watcher = new Watcher(vm, function () {

      vm._update(vm._render(), hydrating); //先产生vnode,再更新组件页面

        

根组件watcher/update方法何时如何被执行?

new Vue()初始化根组件时即会执行,根组件有属性变化时也会触发执行。

 

keep-alive组件的watcher/update方法何时如何被执行?????

总不能写vm._update()吧? (vm假定是keep-alive组件实例)

keep-alive组件没有template没有data,没法用data属性触发执行watcher/update吧?

答案是在源代码中当初始化keep-alive组件的vnode时(也就是执行vnode.data.hook.prepatch方法)会强制执行vm._update()更新keep-alive组件极其页面,其中vm是keep-alive组件,keep-alive组件的页面就是

路由组件页面,router-view负责切换路由组件并且做为keep-alive的子组件,在keep-alive创建vnode时传递路由组件,然后保存在keep-alive vnode的componentOptions的children中,keep-alive和router-view都是占位/管理组件,它有子节点就是路由组件vnode,keep-alive只负责处理缓存,而router-view负责路由组件切换,也就是创建一个新的路由组件,并且更新页面,但当外套<keep-alive>时,router-view不再处理替换,而是把新建的路由组件vnode传递给keep-alive,keep-alive可以从缓存恢复路由组件的实例,然后再更新页面。

 

 

2)根组件的_route属性

从$router.push()开始路由切换,先执行transitionto()以及confirmtransition,这是巨大的坑,这个过程只是处理辅助功能,主要是执行leave和beforeEnter等钩子函数,钩子函数可有可无,这段代码99%都可以

不起任何作用,但看这段代码跟看天书一样,已经有滴滴高手分析了这段代码。

执行transitionto最后会执行回调,在回调代码中会设置根组件的_route属性=当前路由,这是一个关键点,

vue已经针对根组件的_route属性建立了watcher,当set这个属性时,会执行wacther/update,也就是执行

       vm._update(vm._render(), hydrating) (其中vm是根组件)

就是从这里开始真正的路由切换处理,首先执行_render()产生根组件的vnode,再执行_update(vnode)方法调用__patch__(vnode)方法更新根组件页面。

假定页面是这样写的:

       <keep-alive>

              <router-view></router-view>

       </keep-alive>

                    

执行_render()方法时,大家首先要知道根组件template编译之后产生的render/code包含有:

       _c(‘keep-alive‘,[_c(‘router-view‘)])

 

首先会执行_c(‘router-view‘)产生router-view的vnode,_c方法会调用_createElement()方法,再调用

createComponent方法(注意有两个createComponent方法),router-view是functionalComponent,会调用createFunctionalComponent方法,然后执行;

       var vnode = Ctor.options.render.call(null, h, {

其中render就是router-view的render方法,是vue特殊构造的,不同于普通组件的render代码。

router-view的render方从根组件_route属性获取路由,再获取路由组件数据,再创建路由组件vnode返回,这都顺理成章没有什么问题。

 

_c(‘router-view‘)执行完之后要执行_c(‘keep-alive‘,注意写法,_c(‘router-view‘)是keep-alive的子节点,

会把router-view的vnode传递给_c(‘keep-alive‘方法,也就是把路由组件vnode传递给_c(‘keep-alive‘,我们来看一下_createElement()代码,这是vue 2.0最关键最难理解的函数代码:

       function _createElement (

              context,

              tag,

              data,

              children,

              needNormalization

              ) {

会调用createComponent方法,其中有一段代码:

       var vnode = new VNode(

    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : ‘‘)),

    data, undefined, undefined, undefined, context,

    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children }

  );

  return vnode

 

这就是创建keep-alive组件的vnode,其中tag是"vue-componet-3-keep-alive",children就是路由组件的vnode,context就是keep-alive组件实例(keep-alive组件在初始化根组件时就已经建立一直存在)。

大家可以去看一下function VNode()的代码,其中第七个参数就是componentOptions。

这样keep-alive的vnode就创建了,其中有componentOptions也就是路由组件vnode,这是router-view传递而来的,router-view负责路由切换,只有router-view能创建路由组件vnode,但当它外套<keep-alive>

时,它做为keep-alive组件的子节点传递路由组件vnode,而keep-alive取代它成为占位组件占据根组件vnode树中的那个位置。

 

到这里跟组件vnode树中就多了一个vnode,就是路由组件vnode,路由组件vnode已经成功插入vnode树。

我们再回到根组件watcher/update方法,执行完_render()产生vnode之后就执行_update(vnode)方法更新根组件页面,会调用__patch__方法更新根组件页面,对于每一个vnode,会调用patchVnode方法处理,patchVnode会递归每一个vnode,而__patch__方法只是更新组件页面,不递归vnode树。

 

在根组件vnode树种,keep-alive是最底层的vnode,没有子vnode,但它有componentOptions,就是路由组件vnode,keep-alive的使命就是把自身vnode放在自己占的位置上,而vnode中含路由组件vnode,这是非常

关键非常难懂的环节,请继续看下文。

 

继续patch过程,当执行__patch__/patchVnode更新根组件页面时,当执行到keep-alive的那个vnode时,它有data.hook,会执行vnode.data.hook.prepatch()方法,这个方法会执行_updateFromParent方法,这个方法

的名称跟天书一样难理解,其中有以下代码:

       if (hasChildren) {

      vm.$slots = resolveSlots(renderChildren, parentVnode.context); //保存路由组件vnode到keep-alive组件

      vm.$forceUpdate(); //强制keep-alive组件更新显示新的路由组件页面

   

 

这就是把路由组件vnode保存到keep-alive组件实例的$slots中,然后执行keep-alive组件的watcher/update:

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

先执行keep-alive的_render方法,这是vue组件通用方法,有以下代码:

       vnode = render.call(vm._renderProxy, vm.$createElement);

其中render就是keep-alive组件的render方法,其中有以下代码:

       var KeepAlive = {

              render: function render () {

                     var vnode = getFirstComponentChild(this.$slots.default);

 

它是从自身实例的$slots取路由组件vnode返回,再执行update(vnode)更新keep-alive组件页面,此时vnode是路由组件vnode,那么页面就更新为路由组件页面。

之前在执行_c(‘keep-alive‘时已经创建keep-alive vnode返回,然后执行vnode.data.hook.prepatch()处理,这里又把keep-alive vnode替换更新为路由组件vnode,路由组件vnode的parent是keep-alivevnode,但在vnode树中keep-alive vnode并没有子vnode(children),它是一个占位组件vnode,路由切换时它变换vnode为路由组件vnode,页面更新显示的是路由组件页面,有没有晕?

 

再小结一下:

程序中触发路由切换是从修改_route属性开始;

顺便提一下,router中绑定hashchange/pushState是为了针对直接修改浏览器地址栏的情况。

 

transitionto是跑龙套的“骗人”的,不是关键代码,别误入歧途;

 

watcher/update是vue触发程序执行的隐蔽的杀手锏,永远要牢记,创建组件时会针对组件new watcher(),顺便提一下,1.0是针对页面表达式new wacther(),不是针对组件new watcher(),组件属性变化时

会自动执行watcher,也可能在源代码中直接执行watcher/update,这就开始一段重要源代码的执行。

 

根组件编译生成的render/code代码决定了一切,尤其是其中的_c()是vue 2.0精华,与1.0完全不同,_c方法是最重要的切入点,源代码中很少有调用_c的,因此createElement()方法不知道何时如何被调用,

以及如何传递参数,那些神奇的参数数据好像是天上掉下来似的,其实都是执行_c()方法调用createElement再传递参数数据,这个过程是系统自动进行的,没有源代码,像native code一样,导致分析源代码到这个环节

就牺牲了。

 

keep-alive是组件,有update方法,router-view不是组件,没有update方法! 它们都有render方法,

一个是根据路由找路由组件数据再产生路由组件vnode,一个是直接取路由组件vnode返回到vnode树中再更新组件页面,逻辑设计很清楚啊。

 

vnode是对象嵌套,以children表示为子节点嵌套,表现为vnode树。

 

watcher/update方法是最重要的切入点,触发一段程序执行的起点,update更新包括新建都是先产生vnode,再根据vnode更新页面,对于有template的组件,vnode就是与html对应的,对于管理/占位组件或标签比如

router-view/keep-alive,有设计好的render代码,其目的其实就是获取路由组件vnode,之后还干嘛?就是update更新路由组件页面。

 

时间关系,可能还有些关键细节没有提及,有问题欢迎交流,文中有错误或不妥之处欢迎拍砖指正,欢迎有兴趣的网友一起来探索js框架的神秘世界。

 

以上是关于vue 2.0 路由切换以及组件缓存源代码重点难点分析的主要内容,如果未能解决你的问题,请参考以下文章

(尚042) vue_缓存路由组件

vue: 关于多路由公用模板,导致组件内数组缓存问题

vue2.0使用axios怎么不用每个组件都得引用

vue中动态路由组件缓存及生命周期函数

vue.js项目构建之vue-router2.0的使用

7.Vue_____keep-alive(结合路由)