# Vue-Router源码阅读笔记

路由的概念相信大部分同学并不陌生,它的作用就是根据不同的路径映射到不同的视图。我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。Vue-Router 的能力十分强大,它支持 hash、history、abstract 3 种路由方式,提供了 <router-link><router-view> 2 种组件,还提供了简单的路由配置和一系列好用的 API。

# 路由注册

Vue.use

Vue 提供了 Vue.use 的全局 API 来注册这些插件

Vue.use 接受一个 plugin 参数,并且维护了一个 _installedPlugins 数组,它存储所有注册过的 plugin;接着又会判断 plugin 有没有定义 install 方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue;最后把 plugin 存储到 installedPlugins 中。

可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install 方法,当我们执行 Vue.use 注册插件的时候,就会执行这个 install 方法,并且在这个 install 方法的第一个参数我们可以拿到 Vue 对象,这样的好处就是作为插件的编写方不需要再额外去import Vue 了。

路由安装

Vue-Router 的入口文件是 src/index.js,其中定义了 VueRouter 类,也实现了 install 的静态方法:VueRouter.install = install,它的定义在 src/install.js 中。

当用户执行 Vue.use(VueRouter) 的时候,实际上就是在执行 install 函数,为了确保 install 逻辑只执行一次,用了 install.installed 变量做已安装的标志位。另外用一个全局的 _Vue 来接收参数 Vue,因为作为 Vue 的插件对 Vue 对象是有依赖的,但又不能去单独去 import Vue,因为那样会增加包体积,所以就通过这种方式拿到 Vue 对象。

Vue-Router 安装最重要的一步就是利用 Vue.mixin 去把 beforeCreate 和 destroyed 钩子函数注入到每一个组件中。

export function initMixin (Vue: GlobalAPI) {
  Vue.mixin = function (mixin: Object) {
    this.options = mergeOptions(this.options, mixin)
    return this
  }
}
1
2
3
4
5
6

它的实现实际上非常简单,就是把要混入的对象通过 mergeOptions 合并到 Vue 的 options 中,由于每个组件的构造函数都会在 extend 阶段合并 Vue.options 到自身的 options 中,所以也就相当于每个组件都定义了 mixin 定义的选项。

接着给 Vue 原型上定义了 $router 和 $route 2 个属性的 get 方法,这就是为什么我们可以在组件实例上可以访问 this.$router 以及 this.$route,接着又通过 Vue.component 方法定义了全局的 <router-link><router-view> 2 个组件,这也是为什么我们在写模板的时候可以使用这两个标签,它们的作用在之后介绍。

最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。

总结

那么到此为止,我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install 方法,我们通过 Vue.use(plugin) 时候,就是在执行 install 方法。Vue-Router 的 install 方法会给每一个组件注入 beforeCreate 和 destoryed 钩子函数,在 beforeCreate 做一些私有属性定义和路由初始化工作,下一节我们就来分析一下 VueRouter 对象的实现和它的初始化工作。

# VueRouter 对象

VueRouter 的实现是一个类,它定义了一些属性和方法,我们先从它的构造函数看,当我们执行 new VueRouter 的时候做了哪些事情。

构造函数定义了一些属性,其中 this.app 表示根 Vue 实例,this.apps 保存持有 $options.router 属性的 Vue 实例,this.options 保存传入的路由配置,this.beforeHooks、 this.resolveHooks、this.afterHooks 表示一些钩子函数,我们之后会介绍,this.matcher 表示路由匹配器,我们之后会介绍,this.fallback 表示在浏览器不支持 history.pushState 的情况下,根据传入的 fallback 配置参数,决定是否回退到hash模式,this.mode 表示路由创建的模式,this.history 表示路由历史的具体的实现实例,它是根据 this.mode 的不同实现不同,它有 History 基类,然后不同的 history 实现都是继承 History。

实例化 VueRouter 后会返回它的实例 router,我们在 new Vue 的时候会把 router 作为配置的属性传入,回顾一下上一节我们讲 beforeCreate 混入的时候有这么一段代码:

beforeCreate() {
  if (isDef(this.$options.router)) {
    // ...
    this._router = this.$options.router
    this._router.init(this)
    // ...
  }
}  
1
2
3
4
5
6
7
8

所以组件在执行 beforeCreate 钩子函数的时候,如果传入了 router 实例,都会执行 router.init 方法。init 的逻辑很简单,它传入的参数是 Vue 实例,然后存储到 this.apps 中;只有根 Vue 实例会保存到 this.app 中,并且会拿到当前的 this.history,根据它的不同类型来执行不同逻辑。

总结

通过这一节的分析,我们大致对 VueRouter 类有了大致了解,知道了它的一些属性和方法,同时了解到在组件的初始化阶段,执行到 beforeCreate 钩子函数的时候会执行 router.init 方法,然后又会执行 history.transitionTo 方法做路由过渡,进而引出了 matcher 的概念,接下来我们先研究一下 matcher 的相关实现。

# matcher

matcher 是一个对象,它对外暴露了 match 和 addRoutes 方法,通过 createMatcher 方法创建返回。

我们需要了解 Location、Route、RouteRecord 等概念。并通过 matcher 的 match 方法,我们会找到匹配的路径 Route,这个对 Route 的切换,组件的渲染都有非常重要的指导意义。下一节我们会回到 transitionTo 方法,看一看路径的切换都做了哪些事情。

# 路径切换

history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下 transitionTo 的实现。

transitionTo 首先根据目标 location 和当前路径 执行 this.router.match 方法去匹配到目标的路径。这里 this.current 是 history 维护的当前路径,它的初始值是在 history 的构造函数中初始化的:

this.current = START
1
export const START = createRoute(null, {
  path: '/'
})
1
2
3

这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current。

拿到新的路径后,那么接下来就会执行 confirmTransition 方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数。

通过执行 confirmTransition 方法,拿到 updated、activated、deactivated 3 个 ReouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。

导航守卫

官方的说法叫导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数。

我们先从整体上看一下这些钩子函数执行的逻辑,首先构造一个队列 queue,它实际上是一个数组;然后再定义一个迭代器函数 iterator;最后再执行 runQueue 方法来执行这个队列。

按照顺序如下:

  1. 在失活的组件里调用离开守卫

  2. 调用全局的 beforeEach 守卫

  3. 在重用的组件里调用 beforeRouteUpdate 守卫

  4. 在激活的路由配置里调用 beforeEnter

  5. 解析异步路由组件

  6. 在被激活的组件里调用 beforeRouteEnter

  7. 调用全局的 beforeResolve 守卫

  8. 调用全局的 afterEach 钩子

url

当我们点击 router-link 的时候,实际上最终会执行 router.push,如下:

push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
  this.history.push(location, onComplete, onAbort)
}
1
2
3

this.history.push 函数,这个函数是子类实现的,不同模式下该函数的实现略有不同。

  1. hash模式

即地址栏 URL 中的 # 符号,它的特点在于:hash 虽然出现 URL 中,但不会被包含在 HTTP 请求中,对后端完全没有影响,不需要后台进行配置,因此改变 hash 不会重新加载页面。

  1. history模式

利用了 HTML5 History Interface 中新增的 pushState()replaceState() 方法(需要特定浏览器支持)。history 模式改变了路由地址,因为需要后台配置地址。

组件

路由最终的渲染离不开组件,Vue-Router 内置了 <router-view> 组件

<router-view> 是一个 functional 组件,它的渲染也是依赖 render 函数,那么 <router-view> 具体应该渲染什么组件呢,首先获取当前的路径:

const route = parent.$route
1

我们之前分析过,在 src/install.js 中,我们给 Vue 的原型上定义了 $route:

Object.defineProperty(Vue.prototype, '$route', {
  get () { return this._routerRoot._route }
})
1
2
3

然后在 VueRouter 的实例执行 router.init 方法的时候,会执行如下逻辑,定义在 src/index.js 中:

history.listen(route => {
  this.apps.forEach((app) => {
    app._route = route
  })
})
1
2
3
4
5

而 history.listen 方法定义在 src/history/base.js 中:

listen (cb: Function) {
  this.cb = cb
}
1
2
3

然后在 updateRoute 的时候执行 this.cb:

updateRoute (route: Route) {
  //. ..
  this.current = route
  this.cb && this.cb(route)
  // ...
}
1
2
3
4
5
6

也就是我们执行 transitionTo 方法最后执行 updateRoute 的时候会执行回调,然后会更新 this.apps 保存的组件实例的 _route 值,this.apps 数组保存的实例的特点都是在初始化的时候传入了 router 配置项,一般的场景数组只会保存根 Vue 实例,因为我们是在 new Vue 传入了 router 实例。$route 是定义在 Vue.prototype 上。每个组件实例访问 $route 属性,就是访问根实例的 _route,也就是当前的路由线路。

<router-view> 是支持嵌套的,回到 render 函数,其中定义了 depth 的概念,它表示 <router-view> 嵌套的深度。每个 <router-view> 在渲染的时候,执行如下逻辑:

data.routerView = true
// ...
while (parent && parent._routerRoot !== parent) {
  if (parent.$vnode && parent.$vnode.data.routerView) {
    depth++
  }
  if (parent._inactive) {
    inactive = true
  }
  parent = parent.$parent
}

const matched = route.matched[depth]
// ...
const component = cache[name] = matched.components[name]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

parent._routerRoot 表示的是根 Vue 实例,那么这个循环就是从当前的 <router-view> 的父节点向上找,一直找到根 Vue 实例,在这个过程,如果碰到了父节点也是 <router-view> 的时候,说明 <router-view> 有嵌套的情况,depth++。遍历完成后,根据当前线路匹配的路径和 depth 找到对应的 RouteRecord,进而找到该渲染的组件。

那么当我们执行 transitionTo 来更改路由线路后,组件是如何重新渲染的呢?在我们混入的 beforeCreate 钩子函数中有这么一段逻辑:

Vue.mixin({
  beforeCreate () {
    if (isDef(this.$options.router)) {
      Vue.util.defineReactive(this, '_route', this._router.history.current)
    }
    // ...
  }
})
1
2
3
4
5
6
7
8

由于我们把根 Vue 实例的 _route 属性定义成响应式的,我们在每个 <router-view> 执行 render 函数的时候,都会访问 parent.$route,如我们之前分析会访问 this._routerRoot._route,触发了它的 getter,相当于 <router-view> 对它有依赖,然后再执行完 transitionTo 后,修改 app._route 的时候,又触发了setter,因此会通知 <router-view> 的渲染 watcher 更新,重新渲染组件。

总结

路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。