• 摘要

关于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. SSH三大框架的整合

    SSH三个框架的知识点 一.Hibernate框架 1. Hibernate的核心配置文件 1.1 数据库信息.连接池配置 1.2 Hibernate信息 1.3 映射配置 1.4 Hibernate ...

  2. ConcurrentHashMap 源码分析

    ConcurrentHashMap 源码分析 1. 前言    终于到这个类了,其实在前面很过很多次这个类,因为这个类代码量比较大,并且涉及到并发的问题,还有一点就是这个代码有些真的晦涩,不好懂.前前 ...

  3. Python中的PYTHONPATH环境变量

    PYTHONPATH是Python中一个重要的环境变量,用于在导入模块的时候搜索路径.可以通过如下方式访问: >>> import sys >>> sys.path ...

  4. wpf研究之道-datagrid控件(1)

    "想要说些什么 又不知从何说起",每当想要写一些关于wpf的文章,总是沉思良久,怕自己写不好.今天我想要说的是wpf中datagrid控件.我们先来看看它在整个类的层次结构:   ...

  5. Python读取配置文件,并连接数据库SQL Server

    用配置文件保存固定的连接数据,改的话比较方便. 1.新建一个配置文件:SQlconfig.config,以数据库为例. 内容如下,当然也可以添加多个 [Database1] database=db_t ...

  6. 基于 HTML5 Canvas 实现的文字动画特效

    前言 文字是网页中最基本的元素,一般我们在网页上都是展示的静态文字,但是就效果来说,还是比较枯燥的.文字淡入淡出的动画效果在项目中非常实用,如果有某些关键的文字,可以通过这种动态的效果来提醒用户阅读. ...

  7. JavaScript(第十二天)【基本包装类型】

    1.基本包装类型概述 2.Boolean类型 3.Number类型 4.String类型 为了便于操作基本类型值,ECMAScript提供了3个特殊的引用类型:Boolean.Number和Strin ...

  8. C语言字符数组作业

    一.PTA实验作业 题目1:7-1 字符串转换成十进制整数 1. 本题PTA提交列表 2. 设计思路 3.代码截图 4.本题调试过程碰到问题及PTA提交列表情况说明. 1.一开始我没想到怎么判断正负的 ...

  9. 1013团队Beta冲刺day4

    项目进展 李明皇 今天解决的进度 因服务器端未完成登录态维护,故无法进行前后端联动. 明天安排 前后端联动调试 林翔 今天解决的进度 因上课和实验室事务未完成登录态维护 明天安排 完成登录态维护 孙敏 ...

  10. 冲刺NO.10

    Alpha冲刺第十天 站立式会议 项目进展 项目核心功能逐步构建完成,测试工作也已开始.主要对部分功能组合进行测试以测试系统可用性. 问题困难 项目的主要困难在这个时间点主要存在于测试工作中,测试工作 ...