vue-router源码分析
# 简介
路由的概念相信大部分同学并不陌生,它的作用就是根据不同的路径映射到不同的视图。我们在用 Vue 开发过实际项目的时候都会用到 Vue-Router 这个官方插件来帮我们解决路由的问题。Vue-Router 的能力十分强大,它支持 hash
、history
、abstract
3 种路由方式,提供了 <router-link>
和 <router-view>
2 种组件,还提供了简单的路由配置和一系列好用的 API。
大部分同学已经掌握了路由的基本使用,但使用的过程中也难免会遇到一些坑,那么这一章我们就来深挖 Vue-Router 的实现细节,一旦我们掌握了它的实现原理,那么就能在开发中对路由的使用更加游刃有余。
同样我们也会通过一些具体的示例来配合讲解,先来看一个最基本使用例子:
<div id="app">
<h1>Hello App!</h1>
<p>
<!-- 使用 router-link 组件来导航. -->
<!-- 通过传入 `to` 属性指定链接. -->
<!-- <router-link> 默认会被渲染成一个 `<a>` 标签 -->
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
</p>
<!-- 路由出口 -->
<!-- 路由匹配到的组件将渲染在这里 -->
<router-view></router-view>
</div>
import Vue from 'vue'
import VueRouter from 'vue-router'
import App from './App'
Vue.use(VueRouter)
// 1. 定义(路由)组件。
// 可以从其他文件 import 进来
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
// 2. 定义路由
// 每个路由应该映射一个组件。 其中"component" 可以是
// 通过 Vue.extend() 创建的组件构造器,
// 或者,只是一个组件配置对象。
// 我们晚点再讨论嵌套路由。
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
// 3. 创建 router 实例,然后传 `routes` 配置
// 你还可以传别的配置参数, 不过先这么简单着吧。
const router = new VueRouter({
routes // (缩写)相当于 routes: routes
})
// 4. 创建和挂载根实例。
// 记得要通过 router 配置参数注入路由,
// 从而让整个应用都有路由功能
const app = new Vue({
el: '#app',
render(h) {
return h(App)
},
router
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
这是一个非常简单的例子,接下来我们先从 Vue.use(VueRouter)
说起。
# 路由注册
Vue 从它的设计上就是一个渐进式 JavaScript 框架,它本身的核心是解决视图渲染的问题,其它的能力就通过插件的方式来解决。Vue-Router 就是官方维护的路由插件,在介绍它的注册实现之前,我们先来分析一下 Vue 通用的插件注册原理。
# Vue.use
Vue 提供了 Vue.use
的全局 API 来注册这些插件,所以我们先来分析一下它的实现原理,定义在 vue/src/core/global-api/use.js
中:
export function initUse (Vue: GlobalAPI) {
Vue.use = function (plugin: Function | Object) {
const installedPlugins = (this._installedPlugins || (this._installedPlugins = []))
if (installedPlugins.indexOf(plugin) > -1) {
return this
}
const args = toArray(arguments, 1)
args.unshift(this)
//类型判断后调用插件install方法
if (typeof plugin.install === 'function') {
//一般走这里,会看到最终调用插件提供的install静态方法执行插件安装
plugin.install.apply(plugin, args)
} else if (typeof plugin === 'function') {
plugin.apply(null, args)
}
installedPlugins.push(plugin)
return this
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Vue.use
接受一个 plugin
参数,并且维护了一个 _installedPlugins
数组,它存储所有注册过的 plugin
;接着又会判断 plugin
有没有定义 install
方法,如果有的话则调用该方法,并且该方法执行的第一个参数是 Vue
;最后把 plugin
存储到 installedPlugins
中。
可以看到 Vue 提供的插件注册机制很简单,每个插件都需要实现一个静态的 install
方法,当我们执行 Vue.use
注册插件的时候,就会执行这个 install
方法,并且在这个 install
方法的第一个参数我们可以拿到 Vue
对象,这样的好处就是作为插件的编写方不需要再额外去import Vue
,保存vue版本一致。
# 路由安装
Vue-Router 的入口文件是 src/index.js
,其中定义了 VueRouter
类,也实现了 install
的静态方法:VueRouter.install = install
,它的定义在src/install.js
中。
import { install } from './install'
export default class VueRouter {
//声明一个 install 静态方法,安装插件的 install方法会赋给它。
static install: () => void
}
...
//将安装该插件(vue-router)的install方法赋给VueRouter中声明的静态方法install,
//供Vue.use()方法内部调用,来执行插件的安装
VueRouter.install = install
2
3
4
5
6
7
8
9
10
11
install.js
export let _Vue
export function install (Vue) {
if (install.installed && _Vue === Vue) return
install.installed = true //标识安装过
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
//等同于 vm.$options._parentVnode.data.registerRouteInstance(vm,callVal)
}
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
//定义路由钩子函数(组件中的)的合并策略,合并策略跟created钩子函数一样。
const strats = Vue.config.optionMergeStrategies
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
**当用户执行 Vue.use(VueRouter)
的时候,实际上就是在执行 install
函数,为了确保 install
逻辑只执行一次,用了 install.installed
变量做已安装的标志位。**另外用一个全局的 _Vue
来接收参数 Vue
,因为作为 Vue 的插件对 Vue
对象是有依赖的,但又不能去单独去 import Vue
,因为那样会增加包体积,所以就通过这种方式拿到 Vue
对象。
Vue-Router 安装最重要的一步就是利用 Vue.mixin
去把 beforeCreate
和 destroyed
钩子函数注入到每一个组件中。Vue.mixin
的定义,在 vue/src/core/global-api/mixin.js
中:
export function initMixin (Vue: GlobalAPI) {
Vue.mixin = function (mixin: Object) {
this.options = mergeOptions(this.options, mixin)
return this
}
}
2
3
4
5
6
它的实现实际上非常简单,就是把要混入的对象通过 mergeOptions
合并到 Vue
的 options
中,由于每个组件的构造函数都会在 extend
阶段合并 Vue.options
到自身的 options
中,所以也就相当于每个组件都定义了 mixin
定义的选项。
回到 Vue-Router
的 install
方法,先看混入的 beforeCreate
钩子函数,对于根 Vue
实例而言,执行该钩子函数时定义了 this._routerRoot
表示它自身;this._router
表示 VueRouter
的实例 router
,它是在 new Vue
的时候传入的;另外执行了 this._router.init()
方法初始化 router
,这个逻辑之后介绍,然后用 defineReactive
方法把 this._route
变成响应式对象,这个作用我们之后会介绍。而对于子组件而言,由于组件是树状结构,在遍历组件树的过程中,它们在执行该钩子函数的时候 this._routerRoot
始终指向的离它最近的传入了 router
对象作为配置而实例化的父实例。
对于 beforeCreate
和 destroyed
钩子函数,它们都会执行 registerInstance
方法,这个方法的作用我们也是之后会介绍。
接着给 Vue 原型上定义了 $router
和 $route
2 个属性的 get 方法,这就是为什么我们可以在组件实例上可以访问 this.$router
以及 this.$route
,它们的作用之后介绍。
接着又通过 Vue.component
方法定义了全局的 <router-link>
和 <router-view>
2 个组件,这也是为什么我们在写模板的时候可以使用这两个标签,它们的作用也是之后介绍。
最后定义了路由中的钩子函数的合并策略,和普通的钩子函数一样。
兼容其他加载方式
// 兼容用script标签引用的方法
if (inBrowser && window.Vue) {
window.Vue.use(VueRouter);
}
// 和vuex的判断类似
// if (!Vue && typeof window !== "undefined" && window.Vue) {
// install(window.Vue);
// }
2
3
4
5
6
7
8
9
# 总结
那么到此为止,我们分析了 Vue-Router 的安装过程,Vue 编写插件的时候通常要提供静态的 install
方法,我们通过 Vue.use(plugin)
时候,就是在执行 install
方法。Vue-Router
的 install
方法会给每一个组件注入 beforeCreate
和 destoryed
钩子函数,在 beforeCreate
做一些私有属性定义和路由初始化工作,下一节我们就来分析一下 VueRouter
对象的实现和它的初始化工作。
# VueRouter 对象
VueRouter 的实现是一个类,我们先对它做一个简单地分析,它的定义在 src/index.js
中:
export default class VueRouter {
static install: () => void;
static version: string;
app: any;
apps: Array<any>;
ready: boolean;
readyCbs: Array<Function>;
options: RouterOptions;
mode: string;
history: HashHistory | HTML5History | AbstractHistory;
matcher: Matcher;
fallback: boolean;
beforeHooks: Array<?NavigationGuard>;
resolveHooks: Array<?NavigationGuard>;
afterHooks: Array<?AfterNavigationHook>;
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
get currentRoute (): ?Route {
return this.history && this.history.current
}
init (app: any) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
onReady (cb: Function, errorCb?: Function) {
this.history.onReady(cb, errorCb)
}
onError (errorCb: Function) {
this.history.onError(errorCb)
}
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
go (n: number) {
this.history.go(n)
}
back () {
this.go(-1)
}
forward () {
this.go(1)
}
getMatchedComponents (to?: RawLocation | Route): Array<any> {
const route: any = to
? to.matched
? to
: this.resolve(to).route
: this.currentRoute
if (!route) {
return []
}
return [].concat.apply([], route.matched.map(m => {
return Object.keys(m.components).map(key => {
return m.components[key]
})
}))
}
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
normalizedTo: Location,
resolved: Route
} {
const location = normalizeLocation(
to,
current || this.history.current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route
}
}
addRoutes (routes: Array<RouteConfig>) {
this.matcher.addRoutes(routes)
if (this.history.current !== START) {
this.history.transitionTo(this.history.getCurrentLocation())
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
VueRouter
定义了一些属性和方法,我们先从它的构造函数看,当我们执行 new VueRouter
的时候做了哪些事情。
constructor (options: RouterOptions = {}) {
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
this.fallback = mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
构造函数定义了一些属性,其中 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)
// ...
}
}
2
3
4
5
6
7
8
所以组件在执行 beforeCreate
钩子函数的时候,如果传入了 router
实例,都会执行 router.init
方法:
init (app: any) {
process.env.NODE_ENV !== 'production' && assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
if (history instanceof HTML5History) {
history.transitionTo(history.getCurrentLocation())
} else if (history instanceof HashHistory) {
const setupHashListener = () => {
history.setupListeners()
}
history.transitionTo(
history.getCurrentLocation(),
setupHashListener,
setupHashListener
)
}
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
init
的逻辑很简单,它传入的参数是 Vue
实例,然后存储到 this.apps
中;只有根 Vue
实例会保存到 this.app
中,并且会拿到当前的 this.history
,根据它的不同类型来执行不同逻辑,由于我们平时使用 hash
路由多一些,所以我们先看这部分逻辑,先定义了 setupHashListener
函数,接着执行了 history.transitionTo
方法,它是定义在 History
基类中,代码在 src/history/base.js
:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
// ...
}
2
3
4
我们先不着急去看 transitionTo
的具体实现,先看第一行代码,它调用了 this.router.match
函数:
match (
raw: RawLocation,
current?: Route,
redirectedFrom?: Location
): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
2
3
4
5
6
7
实际上是调用了 this.matcher.match
方法去做匹配,所以接下来我们先来了解一下 matcher
的相关实现。
# 总结
通过这一节的分析,我们大致对 VueRouter
类有了大致了解,知道了它的一些属性和方法,同时了解到在组件的初始化阶段,执行到 beforeCreate
钩子函数的时候会执行 router.init
方法,然后又会执行 history.transitionTo
方法做路由过渡,进而引出了 matcher
的概念,接下来我们先研究一下 matcher
的相关实现。
# matcher
matcher
相关的实现都在 src/create-matcher.js
中,我们先来看一下 matcher
的数据结构:
export type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
};
2
3
4
Matcher
返回了 2 个方法,match
和 addRoutes
,在上一节我们接触到了 match
方法,顾名思义它是做匹配,那么匹配的是什么,在介绍之前,我们先了解路由中重要的 2 个概念,Loaction
和 Route
,它们的数据结构定义在 flow/declarations.js
中。
- Location
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}
2
3
4
5
6
7
8
9
10
Vue-Router 中定义的 Location
数据结构和浏览器提供的 window.location
部分结构有点类似,它们都是对 url
的结构化描述。举个例子:/abc?foo=bar&baz=qux#hello
,它的 path
是 /abc
,query
是 {foo:'bar',baz:'qux'}
。Location
的其他属性我们之后会介绍。
- Route
declare type Route = {
path: string;
name: ?string;
hash: string;
query: Dictionary<string>;
params: Dictionary<string>;
fullPath: string;
matched: Array<RouteRecord>;
redirectedFrom?: string;
meta?: any;
}
2
3
4
5
6
7
8
9
10
11
Route
表示的是路由中的一条线路,它除了描述了类似 Loctaion
的 path
、query
、hash
这些概念,还有 matched
表示匹配到的所有的 RouteRecord
。Route
的其他属性我们之后会介绍。
# createMatcher
在了解了 Location
和 Route
后,我们来看一下 matcher
的创建过程:
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
// ...
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
return {
match,
addRoutes
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
createMatcher
接收 2 个参数,一个是 router
,它是我们 new VueRouter
返回的实例,一个是 routes
,它是用户定义的路由配置,来看一下我们之前举的例子中的配置:
const Foo = { template: '<div>foo</div>' }
const Bar = { template: '<div>bar</div>' }
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
2
3
4
5
6
7
createMathcer
首先执行的逻辑是 const { pathList, pathMap, nameMap } = createRouteMap(routes)
创建一个路由映射表,createRouteMap
的定义在 src/create-route-map
中:
export function createRouteMap (
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>
): {
pathList: Array<string>;
pathMap: Dictionary<RouteRecord>;
nameMap: Dictionary<RouteRecord>;
} {
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route)
})
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
return {
pathList,
pathMap,
nameMap
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
createRouteMap
函数的目标是把用户的路由配置转换成一张路由映射表,它包含 3 个部分,pathList
存储所有的 path
,pathMap
表示一个 path
到 RouteRecord
的映射关系,而 nameMap
表示 name
到 RouteRecord
的映射关系。那么 RouteRecord
到底是什么,先来看一下它的数据结构:
declare type RouteRecord = {
path: string;
regex: RouteRegExp;
components: Dictionary<any>;
instances: Dictionary<any>;
name: ?string;
parent: ?RouteRecord;
redirect: ?RedirectOption;
matchAs: ?string;
beforeEnter: ?NavigationGuard;
meta: any;
props: boolean | Object | Function | Dictionary<boolean | Object | Function>;
}
2
3
4
5
6
7
8
9
10
11
12
13
它的创建是通过遍历 routes
为每一个 route
执行 addRouteRecord
方法生成一条记录,来看一下它的定义:
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
if (process.env.NODE_ENV !== 'production') {
assert(path != null, `"path" is required in a route configuration.`)
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(path || name)} cannot be a ` +
`string id. Use an actual component instead.`
)
}
const pathToRegexpOptions: PathToRegexpOptions = route.pathToRegexpOptions || {}
const normalizedPath = normalizePath(
path,
parent,
pathToRegexpOptions.strict
)
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
if (route.children) {
if (process.env.NODE_ENV !== 'production') {
if (route.name && !route.redirect && route.children.some(child => /^\/?$/.test(child.path))) {
warn(
false,
`Named Route '${route.name}' has a default child route. ` +
`When navigating to this named route (:to="{name: '${route.name}'"), ` +
`the default child route will not be rendered. Remove the name from ` +
`this route and use the name of the default child route for named ` +
`links instead.`
)
}
}
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias)
? route.alias
: [route.alias]
aliases.forEach(alias => {
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/'
)
})
}
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
} else if (process.env.NODE_ENV !== 'production' && !matchAs) {
warn(
false,
`Duplicate named routes definition: ` +
`{ name: "${name}", path: "${record.path}" }`
)
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
我们只看几个关键逻辑,首先创建 RouteRecord
的代码如下:
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
components: route.components || { default: route.component },
instances: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter,
meta: route.meta || {},
props: route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
这里要注意几个点,path
是规范化后的路径,它会根据 parent
的 path
做计算;regex
是一个正则表达式的扩展,它利用了path-to-regexp
这个工具库,把 path
解析成一个正则表达式的扩展,举个例子:
var keys = []
var re = pathToRegexp('/foo/:bar', keys)
// re = /^\/foo\/([^\/]+?)\/?$/i
// keys = [{ name: 'bar', prefix: '/', delimiter: '/', optional: false, repeat: false, pattern: '[^\\/]+?' }]
2
3
4
components
是一个对象,通常我们在配置中写的 component
实际上这里会被转换成 {components: route.component}
;instances
表示组件的实例,也是一个对象类型;parent
表示父的 RouteRecord
,因为我们配置的时候有时候会配置子路由,所以整个 RouteRecord
也就是一个树型结构。
if (route.children) {
// ...
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
2
3
4
5
6
7
8
9
如果配置了 children
,那么递归执行 addRouteRecord
方法,并把当前的 record
作为 parent
传入,通过这样的深度遍历,我们就可以拿到一个 route
下的完整记录。
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
2
3
4
为 pathList
和 pathMap
各添加一条记录。
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
// ...
}
2
3
4
5
6
如果我们在路由配置中配置了 name
,则给 nameMap
添加一条记录。
由于 pathList
、pathMap
、nameMap
都是引用类型,所以在遍历整个 routes
过程中去执行 addRouteRecord
方法,会不断给他们添加数据。那么经过整个 createRouteMap
方法的执行,我们得到的就是 pathList
、pathMap
和 nameMap
。其中 pathList
是为了记录路由配置中的所有 path
,而 pathMap
和 nameMap
都是为了通过 path
和 name
能快速查到对应的 RouteRecord
。
再回到 createMatcher
函数,接下来就定义了一系列方法,最后返回了一个对象。
return {
match,
addRoutes
}
2
3
4
也就是说,matcher
是一个对象,它对外暴露了 match
和 addRoutes
方法。
# addRoutes
addRoutes
方法的作用是动态添加路由配置,因为在实际开发中有些场景是不能提前把路由写死的,需要根据一些条件动态添加路由,所以 Vue-Router 也提供了这一接口:
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
2
3
addRoutes
的方法十分简单,再次调用 createRouteMap
即可,传入新的 routes
配置,由于 pathList
、pathMap
、nameMap
都是引用类型,执行 addRoutes
后会修改它们的值。
# match
function match (
raw: RawLocation,
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router)
const { name } = location
if (name) {
const record = nameMap[name]
if (process.env.NODE_ENV !== 'production') {
warn(record, `Route with name '${name}' does not exist`)
}
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
if (typeof location.params !== 'object') {
location.params = {}
}
if (currentRoute && typeof currentRoute.params === 'object') {
for (const key in currentRoute.params) {
if (!(key in location.params) && paramNames.indexOf(key) > -1) {
location.params[key] = currentRoute.params[key]
}
}
}
if (record) {
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}
} else if (location.path) {
location.params = {}
for (let i = 0; i < pathList.length; i++) {
const path = pathList[i]
const record = pathMap[path]
if (matchRoute(record.regex, location.path, location.params)) {
return _createRoute(record, location, redirectedFrom)
}
}
}
return _createRoute(null, location)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
match
方法接收 3 个参数,其中 raw
是 RawLocation
类型,它可以是一个 url
字符串,也可以是一个 Location
对象;currentRoute
是 Route
类型,它表示当前的路径;redirectedFrom
和重定向相关,这里先忽略。match
方法返回的是一个路径,它的作用是根据传入的 raw
和当前的路径 currentRoute
计算出一个新的路径并返回。
首先执行了 normalizeLocation
,它的定义在 src/util/location.js
中:
export function normalizeLocation (
raw: RawLocation,
current: ?Route,
append: ?boolean,
router: ?VueRouter
): Location {
let next: Location = typeof raw === 'string' ? { path: raw } : raw
if (next.name || next._normalized) {
return next
}
if (!next.path && next.params && current) {
next = assign({}, next)
next._normalized = true
const params: any = assign(assign({}, current.params), next.params)
if (current.name) {
next.name = current.name
next.params = params
} else if (current.matched.length) {
const rawPath = current.matched[current.matched.length - 1].path
next.path = fillParams(rawPath, params, `path ${current.path}`)
} else if (process.env.NODE_ENV !== 'production') {
warn(false, `relative params navigation requires a current route.`)
}
return next
}
const parsedPath = parsePath(next.path || '')
const basePath = (current && current.path) || '/'
const path = parsedPath.path
? resolvePath(parsedPath.path, basePath, append || next.append)
: basePath
const query = resolveQuery(
parsedPath.query,
next.query,
router && router.options.parseQuery
)
let hash = next.hash || parsedPath.hash
if (hash && hash.charAt(0) !== '#') {
hash = `#${hash}`
}
return {
_normalized: true,
path,
query,
hash
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
normalizeLocation
方法的作用是根据 raw
,current
计算出新的 location
,它主要处理了 raw
的两种情况,一种是有 params
且没有 path
,一种是有 path
的,对于第一种情况,如果 current
有 name
,则计算出的 location
也有 name
。
计算出新的 location
后,对 location
的 name
和 path
的两种情况做了处理。
name
有 name
的情况下就根据 nameMap
匹配到 record
,它就是一个 RouterRecord
对象,如果 record
不存在,则匹配失败,返回一个空路径;然后拿到 record
对应的 paramNames
,再对比 currentRoute
中的 params
,把交集部分的 params
添加到 location
中,然后在通过 fillParams
方法根据 record.path
和 location.path
计算出 location.path
,最后调用 _createRoute(record, location, redirectedFrom)
去生成一条新路径,该方法我们之后会介绍。
path
通过 name
我们可以很快的找到 record
,但是通过 path
并不能,因为我们计算后的 location.path
是一个真实路径,而 record
中的 path
可能会有 param
,因此需要对所有的 pathList
做顺序遍历, 然后通过 matchRoute
方法根据 record.regex
、location.path
、location.params
匹配,如果匹配到则也通过 _createRoute(record, location, redirectedFrom)
去生成一条新路径。因为是顺序遍历,所以我们书写路由配置要注意路径的顺序,因为写在前面的会优先尝试匹配。
最后我们来看一下 _createRoute
的实现:
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
if (record && record.redirect) {
return redirect(record, redirectedFrom || location)
}
if (record && record.matchAs) {
return alias(record, location, record.matchAs)
}
return createRoute(record, location, redirectedFrom, router)
}
2
3
4
5
6
7
8
9
10
11
12
13
我们先不考虑 record.redirect
和 record.matchAs
的情况,最终会调用 createRoute
方法,它的定义在 src/uitl/route.js
中:
export function createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: ?Location,
router?: VueRouter
): Route {
const stringifyQuery = router && router.options.stringifyQuery
let query: any = location.query || {}
try {
query = clone(query)
} catch (e) {}
const route: Route = {
name: location.name || (record && record.name),
meta: (record && record.meta) || {},
path: location.path || '/',
hash: location.hash || '',
query,
params: location.params || {},
fullPath: getFullPath(location, stringifyQuery),
matched: record ? formatMatch(record) : []
}
if (redirectedFrom) {
route.redirectedFrom = getFullPath(redirectedFrom, stringifyQuery)
}
return Object.freeze(route)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
createRoute
可以根据 record
和 location
创建出来,最终返回的是一条 Route
路径,我们之前也介绍过它的数据结构。在 Vue-Router 中,所有的 Route
最终都会通过 createRoute
函数创建,并且它最后是不可以被外部修改的。Route
对象中有一个非常重要属性是 matched
,它通过 formatMatch(record)
计算而来:
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}
2
3
4
5
6
7
8
可以看它是通过 record
循环向上找 parent
,直到找到最外层,并把所有的 record
都 push 到一个数组中,最终返回的就是 record
的数组,它记录了一条线路上的所有 record
。matched
属性非常有用,它为之后渲染组件提供了依据。
# 总结
那么到此,matcher
相关的主流程的分析就结束了,我们了解了 Location
、Route
、RouteRecord
等概念。并通过 matcher
的 match
方法,我们会找到匹配的路径 Route
,这个对 Route
的切换,组件的渲染都有非常重要的指导意义。下一节我们会回到 transitionTo
方法,看一看路径的切换都做了哪些事情。
# 路径切换
history.transitionTo
是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法,前一节我们分析了 matcher
的相关实现,知道它是如何找到匹配的新线路,那么匹配到新线路后又做了哪些事情,接下来我们来完整分析一下 transitionTo
的实现,它的定义在 src/history/base.js
中:
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const route = this.router.match(location, this.current)
this.confirmTransition(route, () => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => { cb(route) })
}
}, err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
this.ready = true
this.readyErrorCbs.forEach(cb => { cb(err) })
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
transitionTo
首先根据目标 location
和当前路径 this.current
执行 this.router.match
方法去匹配到目标的路径。这里 this.current
是 history
维护的当前路径,它的初始值是在 history
的构造函数中初始化的:
this.current = START
START
的定义在 src/util/route.js
中:
export const START = createRoute(null, {
path: '/'
})
2
3
这样就创建了一个初始的 Route
,而 transitionTo
实际上也就是在切换 this.current
,稍后我们会看到。
拿到新的路径后,那么接下来就会执行 confirmTransition
方法去做真正的切换,由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition
API 设计成带有成功回调函数和失败回调函数,先来看一下它的定义:
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current
const abort = err => {
if (isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => { cb(err) })
} else {
warn(false, 'uncaught error during route navigation:')
console.error(err)
}
}
onAbort && onAbort(err)
}
if (
isSameRoute(route, current) &&
route.matched.length === current.matched.length
) {
this.ensureURL()
return abort()
}
const {
updated,
deactivated,
activated
} = resolveQueue(this.current.matched, route.matched)
const queue: Array<?NavigationGuard> = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
this.pending = route
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
next(to)
}
})
} catch (e) {
abort(e)
}
}
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
首先定义了 abort
函数,然后判断如果满足计算后的 route
和 current
是相同路径的话,则直接调用 this.ensureUrl
和 abort
,ensureUrl
这个函数我们之后会介绍。
接着又根据 current.matched
和 route.matched
执行了 resolveQueue
方法解析出 3 个队列:
function resolveQueue (
current: Array<RouteRecord>,
next: Array<RouteRecord>
): {
updated: Array<RouteRecord>,
activated: Array<RouteRecord>,
deactivated: Array<RouteRecord>
} {
let i
const max = Math.max(current.length, next.length)
for (i = 0; i < max; i++) {
if (current[i] !== next[i]) {
break
}
}
return {
updated: next.slice(0, i),
activated: next.slice(i),
deactivated: current.slice(i)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
因为 route.matched
是一个 RouteRecord
的数组,由于路径是由 current
变向 route
,那么就遍历对比 2 边的 RouteRecord
,找到一个不一样的位置 i
,那么 next
中从 0 到 i
的 RouteRecord
是两边都一样,则为 updated
的部分;从 i
到最后的 RouteRecord
是 next
独有的,为 activated
的部分;而 current
中从 i
到最后的 RouteRecord
则没有了,为 deactivated
的部分。
拿到 updated
、activated
、deactivated
3 个 ReouteRecord
数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。
# 导航守卫
官方的说法叫导航守卫,实际上就是发生在路由路径切换的时候,执行的一系列钩子函数。
我们先从整体上看一下这些钩子函数执行的逻辑,首先构造一个队列 queue
,它实际上是一个数组;然后再定义一个迭代器函数 iterator
;最后再执行 runQueue
方法来执行这个队列。我们先来看一下 runQueue
的定义,在 src/util/async.js
中:
export function runQueue (queue: Array<?NavigationGuard>, fn: Function, cb: Function) {
const step = index => {
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], () => {
step(index + 1)
})
} else {
step(index + 1)
}
}
}
step(0)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这是一个非常经典的异步函数队列化执行的模式, queue
是一个 NavigationGuard
类型的数组,我们定义了 step
函数,每次根据 index
从 queue
中取一个 guard
,然后执行 fn
函数,并且把 guard
作为参数传入,第二个参数是一个函数,当这个函数执行的时候再递归执行 step
函数,前进到下一个,注意这里的 fn
就是我们刚才的 iterator
函数,那么我们再回到 iterator
函数的定义:
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort()
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' && (
typeof to.path === 'string' ||
typeof to.name === 'string'
))
) {
abort()
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
next(to)
}
})
} catch (e) {
abort(e)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
iterator
函数逻辑很简单,它就是去执行每一个 导航守卫 hook
,并传入 route
、current
和匿名函数,这些参数对应文档中的 to
、from
、next
,当执行了匿名函数,会根据一些条件执行 abort
或 next
,只有执行 next
的时候,才会前进到下一个导航守卫钩子函数中,这也就是为什么官方文档会说只有执行 next
方法来 resolve
这个钩子函数。
那么最后我们来看 queue
是怎么构造的:
const queue: Array<?NavigationGuard> = [].concat(
extractLeaveGuards(deactivated),
this.router.beforeHooks,
extractUpdateHooks(updated),
activated.map(m => m.beforeEnter),
resolveAsyncComponents(activated)
)
2
3
4
5
6
7
按照顺序如下:
- 在失活的组件里调用离开守卫。
- 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 - 在激活的路由配置里调用
beforeEnter
。 - 解析异步路由组件。
接下来我们来分别介绍这 5 步的实现。
第一步是通过执行 extractLeaveGuards(deactivated)
,先来看一下 extractLeaveGuards
的定义:
function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}
2
3
它内部调用了 extractGuards
的通用方法,可以从 RouteRecord
数组中提取各个阶段的守卫:
function extractGuards (
records: Array<RouteRecord>,
name: string,
bind: Function,
reverse?: boolean
): Array<?Function> {
const guards = flatMapComponents(records, (def, instance, match, key) => {
const guard = extractGuard(def, name)
if (guard) {
return Array.isArray(guard)
? guard.map(guard => bind(guard, instance, match, key))
: bind(guard, instance, match, key)
}
})
return flatten(reverse ? guards.reverse() : guards)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
这里用到了 flatMapComponents
方法去从 records
中获取所有的导航,它的定义在 src/util/resolve-components.js
中:
export function flatMapComponents (
matched: Array<RouteRecord>,
fn: Function
): Array<?Function> {
return flatten(matched.map(m => {
return Object.keys(m.components).map(key => fn(
m.components[key],
m.instances[key],
m, key
))
}))
}
export function flatten (arr: Array<any>): Array<any> {
return Array.prototype.concat.apply([], arr)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
flatMapComponents
的作用就是返回一个数组,数组的元素是从 matched
里获取到所有组件的 key
,然后返回 fn
函数执行的结果,flatten
作用是把二维数组拍平成一维数组。
那么对于 extractGuards
中 flatMapComponents
的调用,执行每个 fn
的时候,通过 extractGuard(def, name)
获取到组件中对应 name
的导航守卫:
function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
def = _Vue.extend(def)
}
return def.options[key]
}
2
3
4
5
6
7
8
9
获取到 guard
后,还会调用 bind
方法把组件的实例 instance
作为函数执行的上下文绑定到 guard
上,bind
方法的对应的是 bindGuard
:
function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}
2
3
4
5
6
7
那么对于 extractLeaveGuards(deactivated)
而言,获取到的就是所有失活组件中定义的 beforeRouteLeave
钩子函数。
第二步是 this.router.beforeHooks
,在我们的 VueRouter
类中定义了 beforeEach
方法,在 src/index.js
中:
beforeEach (fn: Function): Function {
return registerHook(this.beforeHooks, fn)
}
function registerHook (list: Array<any>, fn: Function): Function {
list.push(fn)
return () => {
const i = list.indexOf(fn)
if (i > -1) list.splice(i, 1)
}
}
2
3
4
5
6
7
8
9
10
11
当用户使用 router.beforeEach
注册了一个全局守卫,就会往 router.beforeHooks
添加一个钩子函数,这样 this.router.beforeHooks
获取的就是用户注册的全局 beforeEach
守卫。
第三步执行了 extractUpdateHooks(updated)
,来看一下 extractUpdateHooks
的定义:
function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}
2
3
和 extractLeaveGuards(deactivated)
类似,extractUpdateHooks(updated)
获取到的就是所有重用的组件中定义的 beforeRouteUpdate
钩子函数。
第四步是执行 activated.map(m => m.beforeEnter)
,获取的是在激活的路由配置中定义的 beforeEnter
函数。
第五步是执行 resolveAsyncComponents(activated)
解析异步组件,先来看一下 resolveAsyncComponents
的定义,在 src/util/resolve-components.js
中:
export function resolveAsyncComponents (matched: Array<RouteRecord>): Function {
return (to, from, next) => {
let hasAsync = false
let pending = 0
let error = null
flatMapComponents(matched, (def, _, match, key) => {
if (typeof def === 'function' && def.cid === undefined) {
hasAsync = true
pending++
const resolve = once(resolvedDef => {
if (isESModule(resolvedDef)) {
resolvedDef = resolvedDef.default
}
def.resolved = typeof resolvedDef === 'function'
? resolvedDef
: _Vue.extend(resolvedDef)
match.components[key] = resolvedDef
pending--
if (pending <= 0) {
next()
}
})
const reject = once(reason => {
const msg = `Failed to resolve async component ${key}: ${reason}`
process.env.NODE_ENV !== 'production' && warn(false, msg)
if (!error) {
error = isError(reason)
? reason
: new Error(msg)
next(error)
}
})
let res
try {
res = def(resolve, reject)
} catch (e) {
reject(e)
}
if (res) {
if (typeof res.then === 'function') {
res.then(resolve, reject)
} else {
const comp = res.component
if (comp && typeof comp.then === 'function') {
comp.then(resolve, reject)
}
}
}
}
})
if (!hasAsync) next()
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
resolveAsyncComponents
返回的是一个导航守卫函数,有标准的 to
、from
、next
参数。它的内部实现很简单,利用了 flatMapComponents
方法从 matched
中获取到每个组件的定义,判断如果是异步组件,则执行异步组件加载逻辑,这块和我们之前分析 Vue
加载异步组件很类似,加载成功后会执行 match.components[key] = resolvedDef
把解析好的异步组件放到对应的 components
上,并且执行 next
函数。
这样在 resolveAsyncComponents(activated)
解析完所有激活的异步组件后,我们就可以拿到这一次所有激活的组件。这样我们在做完这 5 步后又做了一些事情:
runQueue(queue, iterator, () => {
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort()
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
})
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫。 - 调用全局的
afterEach
钩子。
对于第六步有这些相关的逻辑:
const postEnterCbs = []
const isValid = () => this.current === route
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid)
function extractEnterGuards (
activated: Array<RouteRecord>,
cbs: Array<Function>,
isValid: () => boolean
): Array<?Function> {
return extractGuards(activated, 'beforeRouteEnter', (guard, _, match, key) => {
return bindEnterGuard(guard, match, key, cbs, isValid)
})
}
function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string,
cbs: Array<Function>,
isValid: () => boolean
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
next(cb)
if (typeof cb === 'function') {
cbs.push(() => {
poll(cb, match.instances, key, isValid)
})
}
})
}
}
function poll (
cb: any,
instances: Object,
key: string,
isValid: () => boolean
) {
if (instances[key]) {
cb(instances[key])
} else if (isValid()) {
setTimeout(() => {
poll(cb, instances, key, isValid)
}, 16)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
extractEnterGuards
函数的实现也是利用了 extractGuards
方法提取组件中的 beforeRouteEnter
导航钩子函数,和之前不同的是 bind
方法的不同。文档中特意强调了 beforeRouteEnter
钩子函数中是拿不到组件实例的,因为当守卫执行前,组件实例还没被创建,但是我们可以通过传一个回调给 next
来访问组件实例。在导航被确认的时候执行回调,并且把组件实例作为回调方法的参数:
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
2
3
4
5
来看一下这是怎么实现的。
在 bindEnterGuard
函数中,返回的是 routeEnterGuard
函数,所以在执行 iterator
中的 hook
函数的时候,就相当于执行 routeEnterGuard
函数,那么就会执行我们定义的导航守卫 guard
函数,并且当这个回调函数执行的时候,首先执行 next
函数 rersolve
当前导航钩子,然后把回调函数的参数,它也是一个回调函数用 cbs
收集起来,其实就是收集到外面定义的 postEnterCbs
中,然后在最后会执行:
if (this.router.app) {
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => { cb() })
})
}
2
3
4
5
在根路由组件重新渲染后,遍历 postEnterCbs
执行回调,每一个回调执行的时候,其实是执行 poll(cb, match.instances, key, isValid)
方法,因为考虑到一些了路由组件被套 transition
組件在一些缓动模式下不一定能拿到实例,所以用一个轮询方法不断去判断,直到能获取到组件实例,再去调用 cb
,并把组件实例作为参数传入,这就是我们在回调函数中能拿到组件实例的原因。
第七步是获取 this.router.resolveHooks
,这个和 this.router.beforeHooks
的获取类似,在我们的 VueRouter
类中定义了 beforeResolve
方法:
beforeResolve (fn: Function): Function {
return registerHook(this.resolveHooks, fn)
}
2
3
当用户使用 router.beforeResolve
注册了一个全局守卫,就会往 router.resolveHooks
添加一个钩子函数,这样 this.router.resolveHooks
获取的就是用户注册的全局 beforeResolve
守卫。
第八步是在最后执行了 onComplete(route)
后,会执行 this.updateRoute(route)
方法:
updateRoute (route: Route) {
const prev = this.current
this.current = route
this.cb && this.cb(route)
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}
2
3
4
5
6
7
8
同样在我们的 VueRouter
类中定义了 afterEach
方法:
afterEach (fn: Function): Function {
return registerHook(this.afterHooks, fn)
}
2
3
当用户使用 router.afterEach
注册了一个全局守卫,就会往 router.afterHooks
添加一个钩子函数,这样 this.router.afterHooks
获取的就是用户注册的全局 afterHooks
守卫。
那么至此我们把所有导航守卫的执行分析完毕了,我们知道路由切换除了执行这些钩子函数,从表象上有 2 个地方会发生变化,一个是 url 发生变化,一个是组件发生变化。接下来我们分别介绍这两块的实现原理。
# url
当我们点击 router-link
的时候,实际上最终会执行 router.push
,如下:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
2
3
this.history.push
函数,这个函数是子类实现的,不同模式下该函数的实现略有不同,我们来看一下平时使用比较多的 hash
模式该函数的实现,在 src/history/hash.js
中:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}
2
3
4
5
6
7
8
push
函数会先执行 this.transitionTo
做路径切换,在切换完成的回调函数中,执行 pushHash
函数:
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
2
3
4
5
6
7
supportsPushState
的定义在 src/util/push-state.js
中:
export const supportsPushState = inBrowser && (function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && 'pushState' in window.history
})()
2
3
4
5
6
7
8
9
10
11
12
13
14
如果支持的话,则获取当前完整的 url
,执行 pushState
方法:
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
const history = window.history
try {
if (replace) {
history.replaceState({ key: _key }, '', url)
} else {
_key = genKey()
history.pushState({ key: _key }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
pushState
会调用浏览器原生的 history
的 pushState
接口或者 replaceState
接口,更新浏览器的 url 地址,并把当前 url 压入历史栈中。
然后在 history
的初始化中,会设置一个监听器,监听历史栈的变化:
setupListeners () {
const router = this.router
const expectScroll = router.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll) {
setupScroll()
}
window.addEventListener(supportsPushState ? 'popstate' : 'hashchange', () => {
const current = this.current
if (!ensureSlash()) {
return
}
this.transitionTo(getHash(), route => {
if (supportsScroll) {
handleScroll(this.router, route, current, true)
}
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
当点击浏览器返回按钮的时候,如果已经有 url 被压入历史栈,则会触发 popstate
事件,然后拿到当前要跳转的 hash
,执行 transtionTo
方法做一次路径转换。
同学们在使用 Vue-Router 开发项目的时候,打开调试页面 http://localhost:8080
后会自动把 url 修改为 http://localhost:8080/#/
,这是怎么做到呢?原来在实例化 HashHistory
的时候,构造函数会执行 ensureSlash()
方法:
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
export function getHash (): string {
// We can't use window.location.hash here because it's not
// consistent across browsers - Firefox will pre-decode it!
const href = window.location.href
const index = href.indexOf('#')
return index === -1 ? '' : href.slice(index + 1)
}
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}
function replaceHash (path) {
if (supportsPushState) {
replaceState(getUrl(path))
} else {
window.location.replace(getUrl(path))
}
}
export function replaceState (url?: string) {
pushState(url, true)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
这个时候 path
为空,所以执行 replaceHash('/' + path)
,然后内部会执行一次 getUrl
,计算出来的新的 url
为 http://localhost:8080/#/
,最终会执行 pushState(url, true)
,这就是 url 会改变的原因。
# 组件
# <router-view>
路由最终的渲染离不开组件,Vue-Router 内置了 <router-view>
组件,它的定义在 src/components/view.js
中。
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
data.routerView = true
const h = parent.$createElement
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
let depth = 0
let inactive = false
while (parent && parent._routerRoot !== parent) {
if (parent.$vnode && parent.$vnode.data.routerView) {
depth++
}
if (parent._inactive) {
inactive = true
}
parent = parent.$parent
}
data.routerViewDepth = depth
if (inactive) {
return h(cache[name], data, children)
}
const matched = route.matched[depth]
if (!matched) {
cache[name] = null
return h()
}
const component = cache[name] = matched.components[name]
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
;(data.hook || (data.hook = {})).prepatch = (_, vnode) => {
matched.instances[name] = vnode.componentInstance
}
let propsToPass = data.props = resolveProps(route, matched.props && matched.props[name])
if (propsToPass) {
propsToPass = data.props = extend({}, propsToPass)
const attrs = data.attrs = data.attrs || {}
for (const key in propsToPass) {
if (!component.props || !(key in component.props)) {
attrs[key] = propsToPass[key]
delete propsToPass[key]
}
}
}
return h(component, data, children)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
<router-view>
是一个 functional
组件,它的渲染也是依赖 render
函数,其作用是将路由匹配到的组件渲染出来,其渲染依赖于 render 函数。render 函数中首先会执行一个 while 循环,该循环会从调用当前 <router-view>
组件的父节点开始循环,遇到父节点中的 <routerView>
,就给 depth +1,直到 vue 根节点结束,此时 depth 表示当前组件嵌套线路中使用 <router-view>
的数量,也就是层级,这个 depth 在后面 match 匹配时会用到。
那么 <router-view>
具体应该渲染什么组件呢,首先获取当前的路径:
const route = parent.$route
我们之前分析过,在 src/install.js
中,我们给 Vue 的原型上定义了 $route
:
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
2
3
然后在 VueRouter
的实例执行 router.init
方法的时候,会执行如下逻辑,定义在 src/index.js
中:
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})
2
3
4
5
而 history.listen
方法定义在 src/history/base.js
中:
listen (cb: Function) {
this.cb = cb
}
2
3
然后在 updateRoute
的时候执行 this.cb
:
updateRoute (route: Route) {
//. ..
this.current = route
this.cb && this.cb(route)
// ...
}
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]
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
,进而找到该渲染的组件。
除了找到了应该渲染的组件,还定义了一个注册路由实例的方法:
data.registerRouteInstance = (vm, val) => {
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}
2
3
4
5
6
7
8
9
给 vnode
的 data
定义了 registerRouteInstance
方法,在 src/install.js
中,我们会调用该方法去注册路由的实例:
const registerInstance = (vm, callVal) => {
let i = vm.$options._parentVnode
if (isDef(i) && isDef(i = i.data) && isDef(i = i.registerRouteInstance)) {
i(vm, callVal)
}
}
Vue.mixin({
beforeCreate () {
// ...
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在混入的 beforeCreate
钩子函数中,会执行 registerInstance
方法,进而执行 render
函数中定义的 registerRouteInstance
方法,从而给 matched.instances[name]
赋值当前组件的 vm
实例。
render
函数的最后根据 component
渲染出对应的组件 vonde
:
return h(component, data, children)
那么当我们执行 transitionTo
来更改路由线路后,组件是如何重新渲染的呢?在我们混入的 beforeCreate
钩子函数中有这么一段逻辑:
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
Vue.util.defineReactive(this, '_route', this._router.history.current)
}
// ...
}
})
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
更新,重新渲染组件。
# <router-link>
Vue-Router 还内置了另一个组件 <router-link>
, 它支持用户在具有路由功能的应用中(点击)导航。 通过 to
属性指定目标地址,默认渲染成带有正确链接的 <a>
标签,可以通过配置 tag
属性生成别的标签。另外,当目标路由成功激活时,链接元素自动设置一个表示激活的 CSS 类名。
<router-link>
比起写死的 <a href="...">
会好一些,理由如下:
无论是 HTML5 history
模式还是 hash
模式,它的表现行为一致,所以,当你要切换路由模式,或者在 IE9 降级使用 hash
模式,无须作任何变动。
在 HTML5 history
模式下,router-link
会守卫点击事件,让浏览器不再重新加载页面。
当你在 HTML5 history
模式下使用 base
选项之后,所有的 to 属性都不需要写(基路径)了。
那么接下来我们就来分析它的实现,它的定义在 src/components/link.js
中:
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
exact: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
const classes = {}
const globalActiveClass = router.options.linkActiveClass
const globalExactActiveClass = router.options.linkExactActiveClass
const activeClassFallback = globalActiveClass == null
? 'router-link-active'
: globalActiveClass
const exactActiveClassFallback = globalExactActiveClass == null
? 'router-link-exact-active'
: globalExactActiveClass
const activeClass = this.activeClass == null
? activeClassFallback
: this.activeClass
const exactActiveClass = this.exactActiveClass == null
? exactActiveClassFallback
: this.exactActiveClass
const compareTarget = location.path
? createRoute(null, location, null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget)
classes[activeClass] = this.exact
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
const data: any = {
class: classes
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href }
} else {
const a = findAnchor(this.$slots.default)
if (a) {
a.isStatic = false
const extend = _Vue.util.extend
const aData = a.data = extend({}, a.data)
aData.on = on
const aAttrs = a.data.attrs = extend({}, a.data.attrs)
aAttrs.href = href
} else {
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
<router-link>
标签的渲染也是基于 render
函数,它首先做了路由解析:
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(this.to, current, this.append)
2
3
router.resolve
是 VueRouter
的实例方法,它的定义在 src/index.js
中:
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
normalizedTo: Location,
resolved: Route
} {
const location = normalizeLocation(
to,
current || this.history.current,
append,
this
)
const route = this.match(location, current)
const fullPath = route.redirectedFrom || route.fullPath
const base = this.history.base
const href = createHref(base, fullPath, this.mode)
return {
location,
route,
href,
normalizedTo: location,
resolved: route
}
}
function createHref (base: string, fullPath: string, mode) {
var path = mode === 'hash' ? '#' + fullPath : fullPath
return base ? cleanPath(base + '/' + path) : path
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
它先规范生成目标 location
,再根据 location
和 match
通过 this.match
方法计算生成目标路径 route
,然后再根据 base
、fullPath
和 this.mode
通过 createHref
方法计算出最终跳转的 href
。
解析完 router
获得目标 location
、route
、href
后,接下来对 exactActiveClass
和 activeClass
做处理,当配置 exact
为 true 的时候,只有当目标路径和当前路径完全匹配的时候,会添加 exactActiveClass
;而当目标路径包含当前路径的时候,会添加 activeClass
。
接着创建了一个守卫函数 :
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location)
} else {
router.push(location)
}
}
}
function guardEvent (e) {
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
if (e.defaultPrevented) return
if (e.button !== undefined && e.button !== 0) return
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
if (e.preventDefault) {
e.preventDefault()
}
return true
}
const on = { click: guardEvent }
if (Array.isArray(this.event)) {
this.event.forEach(e => { on[e] = handler })
} else {
on[this.event] = handler
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
最终会监听点击事件或者其它可以通过 prop
传入的事件类型,执行 hanlder
函数,最终执行 router.push
或者 router.replace
函数,它们的定义在 src/index.js
中:
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.push(location, onComplete, onAbort)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
this.history.replace(location, onComplete, onAbort)
}
2
3
4
5
6
7
实际上就是执行了 history
的 push
和 replace
方法做路由跳转。
最后判断当前 tag
是否是 <a>
标签,<router-link>
默认会渲染成 <a>
标签,当然我们也可以修改 tag
的 prop
渲染成其他节点,这种情况下会尝试找它子元素的 <a>
标签,如果有则把事件绑定到 <a>
标签上并添加 href
属性,否则绑定到外层元素本身。
# 总结
那么至此我们把路由的 transitionTo
的主体过程分析完毕了,其他一些分支比如重定向、别名、滚动行为等同学们可以自行再去分析。
路径变化是路由中最重要的功能,我们要记住以下内容:路由始终会维护当前的线路,路由切换的时候会把当前线路切换到目标线路,切换过程中会执行一系列的导航守卫钩子函数,会更改 url,同样也会渲染对应的组件,切换完毕后会把目标线路更新替换当前线路,这样就会作为下一次的路径切换的依据。
# 相关链接
https://ustbhuangyi.github.io/vue-analysis/v2/vue-router
https://github.com/HUYIJUNCODING/vue-router-analysis