Vue Router 源码解析(二)内部路由的跳转

之前的博客我们讲了我们创建VueRouter实例并通过Vue.use应用之后,VueRouter内部如何生成一整套自己的数据结构来存储路由配置的,这次博客我们继续解读Vue Router的源码,大致内容是Vue Router的路由守卫的执行逻辑。

路由守卫

这个是VueRouter的路由守卫的官方地址:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

整个路由变化过程中路由守卫的执行的顺序如下:

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫(2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫(2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

用例

还是先讲一下我们本次博客基于的代码

1
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
import Vue from 'vue'
import VueRouter from 'vue-router'

Vue.use(VueRouter)

const Foo = {
template: '<div>foo</div>',
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
}
}
const Bar = { template: '<div>bar</div>' }
const Baz = { template: '<div>baz</div>' }

const router = new VueRouter({
mode: 'history',
base: __dirname,
routes: [
{ path: '/',
// a single route can define multiple named components
// which will be rendered into <router-view>s with corresponding names.
components: {
default: Foo,
a: Bar,
b: Baz
beforeEnter: (to, from) => {
console.log(bar);
// reject the navigation
return true
},
}
},
{
path: '/other',
components: {
default: Baz,
a: Bar,
b: Foo
}
}
]
})

router.beforeEach(async (to, from) => {
// canUserAccess() 返回 `true` 或 `false`
const canAccess = await canUserAccess(to)
if (!canAccess) return '/login'
})

// router.beforeEach支持第三个参数next
// router.beforeEach((to, from, next) => {
// if (to.name !== 'Login' && !isAuthenticated) next({ name: 'Login' })
// else next()
// })

router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})

// afterEach不支持next
router.afterEach((to, from) => {
sendToAnalytics(to.fullPath)
})

new Vue({
router,
template: `
<div id="app">
<h1>Named Views</h1>
<ul>
<li><router-link to="/">/</router-link></li>
<li><router-link to="/other">/other</router-link></li>
</ul>
<router-view class="view one"></router-view>
<router-view class="view two" name="a"></router-view>
<router-view class="view three" name="b"></router-view>
</div>
`
}).$mount('#app')

源码分析

push

我们从VueRouter.prototype.push开始分析调用路由切换之后发生了什么

1
2
3
4
5
6
7
8
9
10
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
// $flow-disable-line
if (!onComplete && !onAbort && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
this.history.push(location, resolve, reject)
})
} else {
this.history.push(location, onComplete, onAbort)
}
}

三个参数分别为RawLocation,成功时的回调函数以及失败时的回调函数,RawLocation其实就是我们在正式使用时push函数传入的参数类型,具体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
declare type Location = {
_normalized?: boolean;
name?: string;
path?: string;
hash?: string;
query?: Dictionary<string>;
params?: Dictionary<string>;
append?: boolean;
replace?: boolean;
}

declare type RawLocation = string | Location

之前的博客我们this.history会根据mode不同实例化为不同的对象,我们以HTML5History来分析

1
2
3
4
5
6
7
8
push (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(location, route => {
pushState(cleanPath(this.base + route.fullPath))
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
}, onAbort)
}

首先是获取当前的路由:current,它的类型是Route

然后调用了transitionTo函数,传入了两个参数,第一个是location,第二个是回调函数,我们看一下这个transitionTo做了什么

transitionTo

在看transtitionTo函数之前,我们看一下几个工具函数

runQueue

首先是runQueue:

这个函数有三个参数,分别是:

  • queue:一个参数的数组
  • fn:实际执行函数,其参数也有两个,第一个是queue中的每一项,第二个是fn执行完成后的回调函数
  • cb:针对queue中的每一项都执行过fn后的最终的回调函数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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)
}

extractGuards

简单来说,这个函数的的作用就是从传入的一系列RouteRecord中,依次抽取出组件内部的路由守卫,然后将这些路由守卫的上下文,也就是this指针通过apply函数指定为自身,如果最后一个参数为true,那就反转这些路由守卫的顺序。

直接对外暴露的函数是,如extractLeaveGuards,它实际上就是调用extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true),也就是从反激活的一系列组件中,获取beforeRouteLeave函数,拼装起来后再反转。

1
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
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)
}

function extractGuard (
def: Object | Function,
key: string
): NavigationGuard | Array<NavigationGuard> {
if (typeof def !== 'function') {
// extend now so that global mixins are applied.
def = _Vue.extend(def)
}
return def.options[key]
}

function extractLeaveGuards (deactivated: Array<RouteRecord>): Array<?Function> {
return extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
}

function extractUpdateHooks (updated: Array<RouteRecord>): Array<?Function> {
return extractGuards(updated, 'beforeRouteUpdate', bindGuard)
}

function bindGuard (guard: NavigationGuard, instance: ?_Vue): ?NavigationGuard {
if (instance) {
return function boundRouteGuard () {
return guard.apply(instance, arguments)
}
}
}

confirmTransition(第1-8步)

然后是,confirmTransition

这个函数大致分为三部分:

  • 判断是否需要abort,如果需要则退出
  • 生成要执行的路由守卫的数组
  • 依次执行路由守卫的数组

首先是判断一下传入的route和当前的route是否是同一个,如果是的话,直接调用abort并退出

1
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
const current = this.current
this.pending = route
const abort = err => {
// changed after adding errors with
// https://github.com/vuejs/vue-router/pull/3047 before that change,
// redirect and aborted navigation would produce an err == null
if (!isNavigationFailure(err) && isError(err)) {
if (this.errorCbs.length) {
this.errorCbs.forEach(cb => {
cb(err)
})
} else {
if (process.env.NODE_ENV !== 'production') {
warn(false, 'uncaught error during route navigation:')
}
console.error(err)
}
}
onAbort && onAbort(err)
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
this.ensureURL()
if (route.hash) {
handleScroll(this.router, current, route, false)
}
return abort(createNavigationDuplicatedError(current, route))
}

然后是一段生成路由守卫执行顺序的函数逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)

const queue: Array<?NavigationGuard> = [].concat(
// in-component leave guards
extractLeaveGuards(deactivated),
// global before hooks
this.router.beforeHooks,
// in-component update hooks
extractUpdateHooks(updated),
// in-config enter guards
activated.map(m => m.beforeEnter),
// async components
resolveAsyncComponents(activated)
)

第一行的resolveQueue的作用是比较当前的路由和即将跳转的路由之间的变化,它的两个参数的类型都是Array<RouteRecord>,它的逻辑也比较简单,就是比较两个数组之间的差异:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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)
}
}

而queue数字中的路由守卫依次是:

  • 所有 deactivated 组件中的 beforeRouteLeave 函数,然后反转顺序
  • 全局的 beforeHooks 函数
  • 所有 updated 组件中的 beforeRouteUpdate 函数
  • 所有 activated 组件对应的路由配置中的 beforeEnter 函数
  • 加载所有 activated 中异步组件的函数

这五步也对应着一开始我们说的路由守卫执行顺序中的2-6步

得到了正确的路由守卫执行顺序的数组后,就是正式执行了,这里就用到了我们刚才说的工具函数: runQueue 了,代码如下:

1
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
const iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
try {
hook(route, current, (to: any) => {
if (to === false) {
// next(false) -> abort navigation, ensure current URL
this.ensureURL(true)
abort(createNavigationAbortedError(current, route))
} else if (isError(to)) {
this.ensureURL(true)
abort(to)
} else if (
typeof to === 'string' ||
(typeof to === 'object' &&
(typeof to.path === 'string' || typeof to.name === 'string'))
) {
// next('/') or next({ path: '/' }) -> redirect
abort(createNavigationRedirectedError(current, route))
if (typeof to === 'object' && to.replace) {
this.replace(to)
} else {
this.push(to)
}
} else {
// confirm transition and pass on the value
next(to)
}
})
} catch (e) {
abort(e)
}
}

runQueue(queue, iterator, () => {
// wait until async components are resolved before
// extracting in-component enter guards
const enterGuards = extractEnterGuards(activated)
const queue = enterGuards.concat(this.router.resolveHooks)
runQueue(queue, iterator, () => {
if (this.pending !== route) {
return abort(createNavigationCancelledError(current, route))
}
this.pending = null
onComplete(route)
if (this.router.app) {
this.router.app.$nextTick(() => {
handleRouteEntered(route)
})
}
})
})

首先iterator函数,两个参数为

  • 要执行的路由守卫
  • 执行成功以后的回调函数

然后我们的runQueue,嵌套了两层:

  • 第一层首先是传入了刚才我们生成的queue,也就是2-6步的路由守卫函数,fn就是我们的iterator函数,结合我们一开始解读的runQueue的源码,总起来第一层做的事情就是,依次执行我们2-6步的路由守卫,每次执行完成一个通过调用hook中的next来调用runQueue中的step来执行下一个hook
  • 第一层把所有queue执行完成之后的回调函数中,我们又生成了一层runQueue函数,区别就在于这一次,我们传入的queue不同了,这一次我们的queue生成方法为:const enterGuards = extractEnterGuards(activated); const queue = enterGuards.concat(this.router.resolveHooks),两部分组成,第一部分是所有activated组件的 beforeRouteEnter 函数,然后是 this.router.resolveHooks,也就是全局的 beforeResolve ,这里就是路由守卫顺序中的第7,8步
  • 在第二层的runQueue的回调函数中,我们就可以调用confirmTransition的的onComplete函数了

然后我们想要了解这个onComplete做了什么,就要看transitionTo在调用confirmTransition的时候传入了什么函数:

1
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
this.confirmTransition(
route,
() => {
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})

// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
},
err => {
if (onAbort) {
onAbort(err)
}
if (err && !this.ready) {
// Initial redirection should not mark the history as ready yet
// because it's triggered by the redirection instead
// https://github.com/vuejs/vue-router/issues/3225
// https://github.com/vuejs/vue-router/issues/3331
if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
}
)
}

comfirmTransition成功回调(第9-12步)

可以看到,我们的onComplete其实就是这一段:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})

// fire ready cbs once
if (!this.ready) {
this.ready = true
this.readyCbs.forEach(cb => {
cb(route)
})
}
  • 第一步,调用updateRoute函数,这个函数内部其实就是history.listen函数传入的回调函数,这个比较重要,这个回调函数会修改$route的值,而这个值是响应式的,在route-view组件中会使用到,也就是说改变这个值会导致route-view重新调用render函数,也就出发了视图的更新
  • 第二步如果transitionTo有onComplete函数那么调用
  • 依次调用 afterHooks 函数,这个其实就是全局注册的 afterEach 的数组,至此,第11步也完成了

那么最后一步,重新执行beforeRouterEnter,并通过next返回实例是怎么做到的呢?这一点其实和第七步生成的beforeRouterEnter的回调方式有关,这个extractEnterGuards和其另两个 extractLeaveGuards,和 extractUpdateHooks不一样,它的bind函数不是简单的apply,而是调用成功之后再执行next回调函数,具体如下:

1
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
function extractEnterGuards (
activated: Array<RouteRecord>
): Array<?Function> {
return extractGuards(
activated,
'beforeRouteEnter',
(guard, _, match, key) => {
return bindEnterGuard(guard, match, key)
}
)
}

function bindEnterGuard (
guard: NavigationGuard,
match: RouteRecord,
key: string
): NavigationGuard {
return function routeEnterGuard (to, from, next) {
return guard(to, from, cb => {
if (typeof cb === 'function') {
if (!match.enteredCbs[key]) {
match.enteredCbs[key] = []
}
match.enteredCbs[key].push(cb)
}
next(cb)
})
}
}

至此,完成了修改路由之后的1-12的函数执行,并且成功通过响应式的的方法触发了router-view的改变。