vue-router 源码分析-整体流程

在现在单页应用这么火爆的年代,路由已经成为了我们开发应用必不可少的利器;而纵观各大框架,都会有对应的强大路由支持。Vue.js 因其性能、通用、易用、体积、学习成本低等特点已经成为了广大前端们的新宠,而其对应的路由 vue-router 也是设计的简单好用,功能强大。本文就从源码来分析下 Vue.js 官方路由 vue-router 的整体流程。

本文主要以 vue-router 的 2.0.3 版本来进行分析。

首先来张整体的图:

先对整体有个大概的印象,下边就以官方仓库下 examples/basic 基础例子来一点点具体分析整个流程。

目录结构

先来看看整体的目录结构:

和流程相关的主要需要关注点的就是 componentshistory 目录以及 create-matcher.jscreate-route-map.jsindex.jsinstall.js 。下面就从 basic 应用入口开始来分析 vue-router 的整个流程。

入口

首先看应用入口的代码部分:

作为插件

上边代码中关键的第 1 步,利用 Vue.js 提供的插件机制 .use(plugin) 来安装 VueRouter ,而这个插件机制则会调用该 plugin 对象的 install 方法(当然如果该 plugin 没有该方法的话会把 plugin 自身作为函数来调用);下边来看下 vue-router 这个插件具体的实现部分。

VueRouter 对象是在 src/index.js 中暴露出来的,这个对象有一个静态的 install 方法:

可以看到这是一个 Vue.js 插件的经典写法,给插件对象增加 install 方法用来安装插件具体逻辑,同时在最后判断下如果是在浏览器环境且存在 window.Vue 的话就会自动使用插件。

install 在这里是一个单独的模块,继续来看同级下的 src/install.js 的主要逻辑:

这里就会有一些疑问了?

  • 为啥要 export 一个 Vue 引用?

插件在打包的时候是肯定不希望把 vue 作为一个依赖包打进去的,但是呢又希望使用 Vue 对象本身的一些方法,此时就可以采用上边类似的做法,在 install 的时候把这个变量赋值 Vue ,这样就可以在其他地方使用 Vue 的一些方法而不必引入 vue 依赖包(前提是保证 install 后才会使用)。

  • 通过给 Vue.prototype 定义 $router$route 属性就可以把他们注入到所有组件中吗?

在 Vue.js 中所有的组件都是被扩展的 Vue 实例,也就意味着所有的组件都可以访问到这个实例原型上定义的属性。

beforeCreate mixin 这个在后边创建 Vue 实例的时候再细说。

实例化 VueRouter

在入口文件中,首先要实例化一个 VueRouter ,然后将其传入 Vue 实例的 options 中。现在继续来看在 src/index.js 中暴露出来的 VueRouter 类:

里边包含了重要的一步:创建 match 匹配函数。

match 匹配函数

匹配函数是由 src/create-matcher.js 中的 createMatcher 创建的:

具体逻辑后续再具体分析,现在只需要理解为根据传入的 routes 配置生成对应的路由 map,然后直接返回了 match 匹配函数。

继续来看 src/create-route-map.js 中的 createRouteMap 函数:

可以看出主要做的事情就是根据用户路由配置对象生成普通的根据 path 来对应的路由记录以及根据 name 来对应的路由记录的 map,方便后续匹配对应。

实例化 History

这也是很重要的一步,所有的 History 类都是在 src/history/ 目录下,现在呢不需要关心具体的每种 History 的具体实现上差异,只需要知道他们都是继承自 src/history/base.js 中的 History 类的:

实例化完了 VueRouter ,下边就该看看 Vue 实例了。

实例化 Vue

实例化很简单:

options 中传入了 router ,以及模板;还记得上边没具体分析的 beforeCreate mixin 吗,此时创建一个 Vue 实例,对应的 beforeCreate 钩子就会被调用:

具体来说,首先判断实例化时 options 是否包含 router ,如果包含也就意味着是一个带有路由配置的实例被创建了,此时才有必要继续初始化路由相关逻辑。然后给当前实例赋值 _router ,这样在访问原型上的 $router 的时候就可以得到 router 了。

下边来看里边两个关键: router.init 和 定义响应式的 _route 对象。

router.init

然后来看 routerinit 方法就干了哪些事情,依旧是在 src/index.js 中:

可以看到初始化主要就是给 app 赋值,针对于 HTML5HistoryHashHistory 特殊处理,因为在这两种模式下才有可能存在进入时候的不是默认页,需要根据当前浏览器地址栏里的 path 或者 hash 来激活对应的路由,此时就是通过调用 transitionTo 来达到目的;而且此时还有个注意点是针对于 HashHistory 有特殊处理,为什么不直接在初始化 HashHistory 的时候监听 hashchange 事件呢?这个是为了修复 https://github.com/vuejs/vue-router/issues/725 这个 bug 而这样做的,简要来说就是说如果在 beforeEnter 这样的钩子函数中是异步的话, beforeEnter 钩子就会被触发两次,原因是因为在初始化的时候如果此时的 hash 值不是以 / 开头的话就会补上 #/ ,这个过程会触发 hashchange 事件,所以会再走一次生命周期钩子,也就意味着会再次调用 beforeEnter 钩子函数。

来看看这个具体的 transitionTo 方法的大概逻辑,在 src/history/base.js 中:

可以看到整个过程就是执行约定的各种钩子以及处理异步组件问题,这里有一些具体函数具体细节被忽略掉了(后续会具体分析)但是不影响具体理解这个流程。但是需要注意一个概念:路由记录,每一个路由 route 对象都对应有一个 matched 属性,它对应的就是路由记录,他的具体含义在调用 match() 中有处理;通过之前的分析可以知道这个 match 是在 src/create-matcher.js 中的:

路由记录在分析 match 匹配函数那里以及分析过了,这里还需要了解下创建路由对象的 createRoute ,存在于 src/util/route.js 中:

回到之前看的 init ,最后调用了 history.listen 方法:

listen 方法很简单就是设置下当前历史对象的 cb 的值, 在之前分析 transitionTo 的时候已经知道在 history 更新完毕的时候调用下这个 cb 。然后看这里设置的这个函数的作用就是更新下当前应用实例的 _route 的值,更新这个有什么用呢?请看下段落的分析。

defineReactive 定义 _route

继续回到 beforeCreate 钩子函数中,在最后通过 Vue 的工具方法给当前应用实例定义了一个响应式的 _route 属性,值就是获取的 this._router.history.current ,也就是当前 history 实例的当前活动路由对象。给应用实例定义了这么一个响应式的属性值也就意味着如果该属性值发生了变化,就会触发更新机制,继而调用应用实例的 render 重新渲染。还记得上一段结尾留下的疑问,也就是 history 每次更新成功后都会去更新应用实例的 _route 的值,也就意味着一旦 history 发生改变就会触发更新机制调用应用实例的 render 方法进行重新渲染。

router-link 和 router-view 组件

回到实例化应用实例的地方:

可以看到这个实例的 template 中包含了两个自定义组件: router-linkrouter-view

router-view 组件

router-view 组件比较简单,所以这里就先来分析它,他是在源码的 src/components/view.js 中定义的:

可以看到逻辑还是比较简单的,拿到匹配的组件进行渲染就可以了。

router-link 组件

再来看看导航链接组件,他在源码的 src/components/link.js 中定义的:

可以看出 router-link 组件就是在其点击的时候根据设置的 to 的值去调用 routerpush 或者 replace 来更新路由的,同时呢,会检查自身是否和当前路由匹配(严格匹配和包含匹配)来决定自身的 activeClass 是否添加。

小结

整个流程的代码到这里已经分析的差不多了,再来回顾下:

相信整体看完后和最开始的时候看到这张图的感觉是不一样的,且对于 vue-router 的整体的流程了解的比较清楚了。当然由于篇幅有限,这里还有很多细节的地方没有细细分析,后续会根据模块来进行具体的分析。

我来评几句
登录后评论

已发表评论数()

相关站点

+订阅
热门文章