Vue Router Source Code Parsing (2) Jump of Internal Router

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

Route guard

This is the official address of VueRouter’s route guard: https://router.vuejs.org/zh/guide/advanced/navigation-guards.html

The order of execution of route guards during the entire route change process is as follows:

  1. Navigation is triggered.
  2. Call the’beforeRouteLeave 'guard in the inactivated component.
  3. Call the global beforeEach guard.
  4. Call the beforeRouteUpdate guard (2.2 +) in the reused component.
  5. Call’beforeEnter 'in the route configuration.
  6. Parse asynchronous routing components.
  7. Call’beforeRouteEnter 'in the activated component.
  8. Call the global beforeResolve guard (2.5 +).
  9. Navigation is confirmed.
  10. Invoke the global afterEach hook.
  11. Trigger DOM update.
  12. Call the callback function passed to next in the’beforeRouteEnter 'guard, and the created component instance will be passed in as the parameter of the callback function.

Use case

Let’s first talk about the code on which our blog is based

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) {
//called before the corresponding route for rendering this component is verified
//Cannot get component instance'this'!
//Because when the guard executes, the component instance has not yet been created!
},
beforeRouteUpdate(to, from) {
Called when the current route changes but the component is to be reused
//For example, for a path '/users/: id' with dynamic parameters, when jumping between '/users/1' and '/users/2',
//Since the same'UserDetails' component will be rendered, the component instance will be to reuse. And the hook will be called in this case.
//Because the component is already mounted when this happens, the navigation guard can access the component instance'this'.
},
beforeRouteLeave(to, from) {
//Called when navigating away from the corresponding route that renders the component
//Like'beforeRouteUpdate ', it can access the component instance'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 () returns'true 'or'false'
const canAccess = await canUserAccess(to)
if (!canAccess) return '/login'
})

//router.beforeEach supports the third parameter 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) {
//... handle error, then cancel navigation
return false
} else {
Unexpected error, cancel navigation and pass the error to the global processor
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')

Source code analysis

push

We start with VueRouter.prototype.push to analyze what happens after the call route switch

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

The three parameters are RawLocation, the callback function when successful and the callback function when failed. RawLocation is actually the parameter type passed by the push function when we use it formally. The specific definitions are as follows:

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

The first is to get the current route: current, its type is Route

Then call the’transitionTo ‘function, pass in two parameters, the first is the location, the second is the callback function, let’s see what this’transitionTo’ does

transitionTo

Before we look at the transtitionTo function, let’s look at a few utility functions.

runQueue

The first is’runQueue ':

This function has three parameters, which are:

  • queue: an array of parameters
  • fn: The actual execution of the function, which also has two parameters, the first is each item in the queue, and the second is the callback function after fn is executed.
  • cb: the final callback function after executing fn for each item in the queue
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

Simply put, the role of this function is to extract the routing guards inside the component from a series of incoming RouteRecord, and then specify the context of these routing guards, that is, the this pointer through the apply function as itself. If the last parameter is true, it reverses the order of these routing guards.

The function directly exposed to the outside world is, such as extractLeaveGuards, which actually calls’extractGuards (deactivated, ‘beforeRouteLeave’, bindGuard, true) ‘, that is, from a series of deactivated components, get the’beforeRouteLeave’ function, assemble it and then reverse it.

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 (Steps 1-8)

Then, ‘confirmTransition’:

This function is roughly divided into three parts:

  • Determine whether to abort, if necessary, exit
    Generate an array of route guards to execute
  • Sequentially execute an array of route guards

The first is to determine whether the incoming route and the current route are the same. If so, call abort directly and exit

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

Then there is a piece of function logic that generates the execution order of route guards.

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

The role of the first line of’resolveQueue ‘is to compare the current route and the route to jump between the changes, its two parameters are the type of’Array < RouteRecord >’, its logic is relatively simple, is to compare the difference between the two arrays:

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

The routing guards in the queue numbers are in order:

  • beforeRouteLeave function in all deactivated components and then reverse the order
  • global beforeHooks function
  • beforeRouteUpdate function in all updated components
  • beforeEnter function in the routing configuration corresponding to all activated components
  • Load all functions of asynchronous components in activated

These five steps also correspond to steps 2-6 in the execution sequence of the route guard we mentioned at the beginning

After getting the correct array of route guards execution order, it is officially executed. Here, we use the tool function: runQueue we just mentioned. The code is as follows:

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

First, the iterator function has two arguments

  • Route guard to perform
  • Callback function after successful execution

Then our runQueue has two nested layers:

  • The first layer is first passed in the queue we generated just now, that is, the route guard function of 2-6 steps, fn is our iterator function, combined with the runQueue source code we read at the beginning, the first layer does in total The thing is to execute our route guard of 2-6 steps in turn, and each execution is completed by calling the next step in the runQueue to execute the next hook
  • In the callback function after all queues are executed in the first layer, we generate a layer of runQueue function. The difference is that this time, the queue we pass in is different. This time our queue generation method is: 'const enterGuards = extractEnterGuards (activated); const queue = enterGuards.concat (this.router.resolveHooks) ', composed of two parts, the first part is the beforeRouteEnter function of all activated components, and then this.router.resolveHooks, that is, the global beforeResolve, here is the route guard order Step 7 and 8
  • In the callback function of runQueue on the second layer, we can call the onComplete function of confirmTransition

Then we want to understand what this onComplete does, we need to see what function transitionTo passes when calling 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 success callback (steps 9-12)

As you can see, our onComplete is actually this paragraph:

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)
})
}
  • The first step is to call the updateRoute function. The inside of this function is actually the callback function passed in by history.listen function. This is more important. This callback function will modify the value of $route, and this value is responsive. It will be used in the route-view component, that is to say, changing this value will cause route-view to re-call the render function, which will start the update of the view
  • Step 2 If transitionTo has an onComplete function then call
  • call the afterHooks function in turn, which is actually an array of globally registered afterEach, so far, step 11 is also completed

So the last step is to re-execute beforeRouterEnter and return an instance through next. This is actually related to the callback method of beforeRouterEnter generated in the seventh step. This’extractEnterGuards’ and its other two’extractLeaveGuards’ are different from’extractUpdateHooks’. Its bind function is not simply apply, but executes the next callback function after the call is successful, as follows:

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

At this point, the function execution 1-12 after modifying the route has been completed, and the router-view change has been successfully triggered by the reactive method.