• 摘要

关于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框架的神秘世界。

vue 2.0 路由切换以及组件缓存源代码重点难点分析的更多相关文章

  1. vue 2.0 路由创建的详解过程

    <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8&quo ...

  2. vue2.0路由切换后页面滚动位置不变BUG

    最近项目中遇到这样一个问题,vue切换路由,页面到顶端的滚动距离仍会保持不变.  方法一: 监听路由 // app.vue export default { watch:{ '$route':func ...

  3. Vue中解决路由切换,页面不更新的实用方法

    前言:vue-router的切换不同于传统的页面的切换.路由之间的切换,其实就是组件之间的切换,不是真正的页面切换.这也会导致一个问题,就是引用相同组件的时候,会导致该组件无法更新,也就是我们口中的页 ...

  4. Vue 2.0 路由全局守卫

    vue2.0 实现导航守卫(路由守卫) 路由跳转前做一些验证,比如登录验证,是网站中的普遍需求. 对此,vue-route 提供的 beforeRouteUpdate 可以方便地实现导航守卫(navi ...

  5. vue多个路由复用同一个组件的跳转问题(this.router.push)

    因为router-view传参问题无法解决,比较麻烦. 所以我采取的是@click+this.router.push来跳转 但是现在的问题是跳转后,url改变了,但是页面的数据没有重新渲染,要刷新才可 ...

  6. 在vue2.0中引用element-ui组件库

    element-ui是由饿了么团队开发的一套基于 Vue 2.0 的桌面端组件库. 官网:http://element.eleme.io/ 安装 npm i element-ui -S 引用完整的el ...

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

    当多个路由复用同一个模板,此时在这几个路由间切换,模板并不会重新挂载.针对这个情况,我们需要在当前逻辑内对路由做监听,在发生变化时更新对应属性,已满足需求. 但是,在实现的过程中会遇到如下情况: 如图 ...

  8. vue路由切换时内容组件的滚动条回到顶部

    在使用vue的时候会出现切换路由的时候滚动条保持在原来的位置,要切换路由的时候滚动条回到顶部才有更好的用户体验 1.当页面整体都要滚动到顶部的情况 router.afterEach(() => ...

  9. 从壹开始前后端分离 [ Vue2.0+.NET Core2.1] 二十║Vue基础终篇:传值+组件+项目说明

    缘起 新的一天又开始啦,大家也应该看到我的标题了,是滴,Vue基础基本就到这里了,咱们回头看看这一路,如果你都看了,并且都会写了,那么现在你就可以自己写一个Demo了,如果再了解一点路由,ajax请求 ...

随机推荐

  1. RTMP规范协议

    本文参照rtmp协议英文版,进行简单的协议分析 1.什么是RTMP 关于 Adobe 的实时消息协议(Real Time Messaging Protocol,RTMP),是一种多媒体的复用和分组的应 ...

  2. 北京工业大学耿丹学院2016下C作业学习总结

    北京工业大学耿丹学院2016下C的班级地址在https://edu.cnblogs.com/campus/bjgygd/Sixteen-One . 第一次作业:两部分 第一部分:新建博客,书写第一篇随 ...

  3. Beta Scrum Day 4

    听说

  4. 乘法表(24.9.2017) (WARNING!!!!!!!!!!!)

    #include "stdio.h" main() { int i,j,result; printf("\n"); ;i<;i++) { ;j<;j ...

  5. ios swift例子源码网址总结

    http://blog.csdn.net/woaifen3344/article/details/40079351 http://www.ruanman.net/swift/learn/4607.ht ...

  6. 点击tableViewCell,调用打电话的功能

    对于点击tableViewCell,调用打电话的功能,按照一般的方法,使用Appdelegate的OpenUrl的方法,使用前先使用UIAlertView展示,让用户选择是否拨打,但是发现了个简单的方 ...

  7. Struts2之Struts2的下载与安装

    Struts2的下载 登陆struts的官网 下载Full Distribution这个选项的struts2的包. 这是Struts2的完整版,里面包括Struts2的实例应用,空实例应用,核心库,源 ...

  8. bzoj千题计划274:bzoj3779: 重组病毒

    http://www.lydsy.com/JudgeOnline/problem.php?id=3779 有一棵树,初始每个节点有不同的颜色 操作1:根节点到x的路径上的点 染上一种没有出现过的颜色 ...

  9. bzoj千题计划288:bzoj1876: [SDOI2009]SuperGCD

    http://www.lydsy.com/JudgeOnline/problem.php?id=1876 高精压位GCD 对于  GCD(a, b)  a>b 若 a 为奇数,b 为偶数,GCD ...

  10. Mongodb 3 查询优化(慢查询Profiling)

    开启慢查询Profiling Profiling级别说明 0:关闭,不收集任何数据. 1:收集慢查询数据,默认是100毫秒. 2:收集所有数据 1.通过修改配置文件开启Profiling 修改启动mo ...