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

  • 摘要

关于vue 2.0源代码分析,已经有不少文档分析功能代码段比如watcher,history,vnode等,但没有一个是分析重点难点的,没有一个是分析大命题的,比如执行router.push之后到底是如何执行代码实现路由切换的?
本文旨在分享本人研究vue 2.0源代码重点难点之结果,不涉及每段源代码具体分析,源代码功能段每个人都可以去分析,只要有耐心,再参考已有高手发表的源代码分析文档,不是太难,主要是要克服一些编程技术问题,比如嵌套回调,递归,对象/数组特殊处理方法等等。

首先要说的是,vue 2.0的复杂性和难点都是由于采用vnode技术引起的,如果不采用vnode技术,像1.0那样,
就没有这些复杂性和难点。
我们先简单回顾一下vue 1.0的路由切换和组件更新的入口代码,Vue2.0基本上也是用类似的入口机制,但触发机制不同。

  • vue 1.0 组件更新入口代码

vue 1.0会针对页面指令表达式创建watcher:

var watcher = new Watcher(vm, expOrFn, cb, options);
会针对组件的data属性执行响应式方法为属性建立set/get方法:

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源代码分析不是本文目的,网上已经有几个文档分析很透彻,有兴趣可以去查看。

  • vue 2.0路由切换入口代码

vue 2.0从router.push()开始路由切换时执行transitionTo方法开始路由切换流程,但transitionTo方法其实只是处理辅助功能,比如执行leave和beforeEnter钩子函数,真正的路由切换处理代码并不在这儿,而是通过updateRoute方法修改_route属性触发执行真正的路由切换代码。

首先每个组件都会创建new watcher:

vm._watcher = new Watcher(vm, function () {
vm._update(vm._render(), hydrating); //先产生vnode,再更新组件页面

new Vue()初始化根组件时即会执行根组件的_update方法,根组件有属性变化时也会触发执行_update方法,这是vue响应式机制实现的功能,具体细节可以参考已有文档,有1-2篇文档分析非常透彻,vue响应式机制原理已经不再是什么秘密。

说过了根组件,那么有个问题就是keep-alive组件的watcher/update方法何时如何被执行?
首先,keep-alive组件没有template没有data,没法用data属性触发执行watcher/update。
在源代码中当初始化keep-alive组件的vnode时(也就是执行vnode.data.hook.prepatch方法)会强制
执行vm._update()更新keep-alive组件极其页面,其中vm是keep-alive组件,keep-alive组件的页面就是
路由组件页面。
vue 2.0由于采用组件标签<keep-alive><router-view>方式实现路由组件缓存,因此具有以下特殊机制:
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可以从缓存恢复路由组件的实例,然后再更新页面。

我们再从$router.push()开始,从$router.push()开始路由切换,先执行transitionto()以及confirmtransition(),关于这段源代码,已经有滴滴高手发表了详细的分析文档,有兴趣的可以去查看。
执行transitionto最后会执行回调,在回调代码中会设置根组件的_route属性=当前路由,为了启动路由切换入口,vue 2.0专门在根组件设计了一个_route属性,vue已经针对根组件的_route属性建立了watcher,当set这个属性时,会执行wacther/update,也就是执行vm._update(vm._render(), hydrating) (其中vm是根组件),
就是从这里开始进入真正的路由切换处入口,这是一个关键环节,如果没找到这个关键环节,把源代码看来看去,也还是不知道路由切换入口代码在哪里,transitionTo()方法并不处理路由切换。

  • vue 2.0 路由组件切换的缓存机制

从执行vm._update(vm._render(), hydrating)就开始,首先执行_render()产生根组件的vnode,再执行_update(vnode)方法调用patch(vnode)方法更新根组件页面。
vue 2.0规定的页面写法是<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,页面更新显示的是路由组件页面,有没有晕?因为vnode可以是对应html节点,也可以对应组件节点,组件vnode又分为管理组件vnode和应用组件vnode,它们的render方法是不同的,产生的vnode也是不同的,处理方法也是不同的。

  • 小结回顾

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

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

transitionto()方法是跑龙套的非关键代码,它只是处理路由切换之前以及之后执行钩子函数,钩子函数不是必须的,假定没有钩子函数,它实际上就是空运行一遍流程,如果看源代码时把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方法的,它是在编译template生成的render/code中含_c()方法,执行render/code时就会执行其中的_c()方法。

keep-alive是组件,有update方法,router-view不是组件,没有update方法! 它们都有render方法,
一个是根据路由找路由组件数据再产生路由组件vnode,一个是直接取路由组件vnode返回到vnode树中再更新组件页面,逻辑设计很清楚是不是?

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

watcher/update方法是路由切换和页面更新最重要的切入点/入口,update更新包括新建都是先执行_render方法产生vnode,再根据vnode更新页面,对于有template的组件,vnode就是与html对应的,对于管理/占位组件或标签比如router-view/keep-alive,有设计好的render代码,其目的其实就是获取路由组件vnode,之后还干嘛?就是
update更新路由组件页面。

大致逻辑挺简单的,但要把源代码走通很难,因为源代码太分散,设计逻辑和编程技术高超,超出一般想象,
有些源代码是异步同时执行的,有些函数比如_c()方法的调用方法比较隐蔽比较特殊,很难追朔debug看重要关键参数数据是怎么来的,源代码中的注释太少太短,尤其在关键之处甚至没有注释。

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

上一篇:一道面试题比较synchronized和读写锁


下一篇:Git服务器 gitweb与gitLab的区别