Vue源码-Vue-Router

前端路由是构建单页面应用的关键技术,它可以让浏览器URL变化但是不请求服务器的前提下,让页面重新渲染出我们想要的结果。Vue-Router是Vue应用的前端路由插件,让我们来看看它的实现原理。

路由例子

为了可以更好的阅读源码,我们可以用一个路由的简单例子来看下关键步骤的结果:

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
// main.js
import Vue from 'vue';
import VueRouter from 'vue-router';
import App from './App.vue';

Vue.use(VueRouter);

const Foo = {
template: `
<div>
<p>Foo</p>
<router-link to="/foo/bar">Go to Bar</router-link>
<router-view></router-view>
</div>`
};

const Bar = {
template: `<div><p>Bar</p></div>`
};

const routes = [
{
path: '/foo',
component: Foo,
children: [
{
path: 'bar',
component: Bar
}
]
}
];

const router = new VueRouter({
routes
});

new Vue({
el: '#app',
render: h => h(App),
router
});

对应App组件的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<template>
<div class="app">
<h1>Vue Router App</h1>
<p>
<router-link to="/foo">Go to foo</router-link>
</p>
<router-view></router-view>
</div>
</template>

<script>
export default {
name: 'app'
};
</script>

插件安装

Vue为所有插件提供一个Vue.use()来安装注册插件,这个方法会调用插件导出对象的install方法,并把Vue函数作为该函数第一个参数传递。在src/install.js文件中是关于Vue Router的安装程序。安装的过程主要有关键的几步:

  • 通过Vue.mixin()全局混入beforeCreatedestroyed钩子:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
// new Vue 实例
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)
}
})

beforeCreate钩子中,对于根实例,设置_routerRoot为实例本身,_router为VueRouter实例并调用init方法进行初始化,然后通过defineReactive_route属性进行响应处理,这个是路径导航导致视图渲染的关键。对于组件实例,通过父子链关系this.$parent && this.$parent._routerRoot设置_routerRoot属性。在函数最后调用registerInstance主要是把组件的实例和路由规则进行绑定,这个之后会知道用处。

  • Vue.prototype挂载属性
1
2
3
4
5
6
7
8
9
// 每个组件可以vm.$router获取VueRouter实例
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})

// 每个组件可以vm.$route获取当前的路由路径Route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})

这就是为什么我们能在每个组件内通过vm.$routervm.$route方法路由实例和当前路由路径的原因

  • 全局注册路由组件
1
2
3
// 全局注册router-view和router-link组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)

之后我们就可以在任何组件内使用router-link进行路由跳转,使用router-view进行路由组件的挂载。

VueRouter

在进行插件安装后,然后会声明路由配置规则,并通过new VueRouter(options)新建路由实例。在src/index.js定义VueRouter类,在构造函数中先初始化一些属性:

1
2
3
4
5
6
7
this.app = null
this.apps = []
this.options = options
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
this.matcher = createMatcher(options.routes || [], this)

紧接着是对路由模式mode的处理:

1
2
3
4
5
6
7
8
9
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

它默认值为hash。对于设置了history模式,并且浏览器不支持history.pushState并且没有设置不允许自动降级fallback=false,会自动用hash模式替换。如果不在浏览器端会采用abstract,比如在node环境,它主要是用数组的方式来模拟浏览记录栈。最后根据不同的mode来新建History实例,它是路由切换和记录管理的类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 // 根据mode创建不同的history实例
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}`)
}
}

它们都定义在src/history文件下,不同模式的histroy类都继承定义在base.js的基类。

init

在之前路由安装中会通过在全局混入beforeCreate钩子并组件根实例会调用router实例的init方法:

1
this._router.init(this) // 初始化

init方法首先是对注册路由的Vue实例的管理:

1
2
3
4
5
6
this.apps.push(app)
if (this.app) {
return
}

this.app = app

在app有值的前提下,会直接返回。这是为了让路由切换相关事件的绑定只处理一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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在构造阶段已经根据不同mode初始化了,所以它调用不同方式进行初始根路由的跳转。transitionTo是跳转到指定路由位置的入口,setupListeners方法是监听浏览器的url变化,里面实现细节我们后文会分析。

matcher

路由实例有一个关键的属性matcher,它表示一个路由匹配器,是对路由记录和新路由匹配的管理。在构造函数中通过createMatcher方法进行初始化:

1
this.matcher = createMatcher(options.routes || [], this)

这个方法定义在src/create-matcher.js,它定义了一些处理路由记录的方法并最终导出一个具有matchaddRoutes方法的对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// 创建RouteRecord映射
const { pathList, pathMap, nameMap } = createRouteMap(routes)

function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}

// ...

return {
match,
addRoutes
}
}

addRoutes方法是添加路由配置对象,因为我们开发过程中并不是所有路由规则都是事先定义好的。主要是调用createRouteMap方法把routes路由配置规则生成路由记录并存在对应的变量中。路由记录RouteRecord是对我们路由配置对象的格式化对象,它的类型定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
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>;
}

来看下createRouteMap方法定义:

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

// 遍历每个路由配置对象, 生成对象的RouteRecord对象
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
}
}

这里一开始获取传入的存取对象否则新建一些默认值,这就是我们在之后可以添加路由配置的关键。接着遍历每个路由规则配置对象调用addRouteRecord方法。这个方法就是生成一个路由记录对象并存在对应的位置

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
const record: RouteRecord = {
path: normalizedPath,
// 利用path-to-regexp库创建路径正则表达式
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 (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}

if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}

对于嵌套的路由规则,还要遍历chidlren中每个路由规则并递归调用addRouteRecord方法:

1
2
3
4
5
6
7
8
if (route.children) {
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}

最终,pathList就是所有路由规则路径的数组,pathMap就是路径到路由记录的映射,nameMap就是路由名称到路由记录的映射。

对于我们的例子,生成的结果如下:

接着,我们看下match方法的定义。match方法是根据当前的route和响应跳转的位置location计算得出新的route。首先,要对新的跳转位置进行格式化:

1
const location = normalizeLocation(raw, currentRoute, false, router)

因为我们在route-linkto属性或者router.push方法传的可以是一个字符串或者对象,统一把它格式化成location对象。对于它的类型定义:

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

对应有name属性的location,我们直接在nameMap中可以拿到对应的路由记录。然后处理下路由参数生成路由最终的路径:

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
if (name) {
// 根据name拿到路由记录
const record = nameMap[name]
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]
}
}
}

// 生成完成的路径字符串
location.path = fillParams(record.path, location.params, `named route "${name}"`)
return _createRoute(record, location, redirectedFrom)
}

对于只有path属性的位置信息,必须遍历每个记录进行正则匹配,匹配到了还要提取path中的参数到params对象中:

1
2
3
4
5
6
7
8
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)
}
}

两种情况找到对于的路由记录后都会调用_createRoute方法生成路由对象route

1
2
3
4
5
6
7
8
9
10
11
12
13
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)
}

其中redirectalias是对重定向和别名的处理,在它们里面或者正常情况都会调用createRoute来返回一个路由路径对象。

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

路由对象route和位置信息location最大区别它包含一个路由规则匹配的路径信息matched。它通过formatMatch方法生成:

1
2
3
4
5
6
7
8
9
// 构建route中matched,先父后子
function formatMatch (record: ?RouteRecord): Array<RouteRecord> {
const res = []
while (record) {
res.unshift(record)
record = record.parent
}
return res
}

很明显它会通过不断查找记录的父亲,然后丢到数组头部,所以matched的匹配顺序保持先父后子。这个属性在后面的视图渲染尤其重要。最终,我们的match方法就根据新位置信息和当前路由对象得出新的路由对象。

history

在路由的编程导航中,我们可以通过router.push()方法来跳转一个新的路径并渲染新的视图。我们从入口看下整个流程。push是不同类型的history自身的实现,对于HashHistory的定义在src/history/hash.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 编程跳转
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
)
}

这个方法主要调用了基类定义的transitionTo方法:

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
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
const route = this.router.match(location, this.current) // 根据跳转的location计算出新的route
this.confirmTransition(
route,
() => {
// 更新route
this.updateRoute(route)
onComplete && onComplete(route)
this.ensureURL()

// 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) {
this.ready = true
this.readyErrorCbs.forEach(cb => {
cb(err)
})
}
}
)
}

导航守卫

transitionTo方法中先调用match方法计算出新的路由对象,然后调用confirmTransition方法,这个方法主要是处理导航守卫的逻辑。首先会判断新的路由对象是否和老路由对象相等,相等的话会调用aboart函数并退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const current = this.current;
const abort = err => {
if (!isExtendedError(NavigationDuplicated, err) && 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);
};
// 相同的route
if (
isSameRoute(route, current) &&
// in the case the route map has been dynamically appended to
route.matched.length === current.matched.length
) {
this.ensureURL();
return abort(new NavigationDuplicated(route));
}

接着根据新老路由对象计算出需要更新,进入和离开的路由记录:

1
2
3
4
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)

resolveQueue方法的定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 根据新旧的matched得出不同类型的RouteRecord部分
// 比如 /foo/bar 和 /foo/baz 的 matched比较
// /foo => updated /foo/baz => activated /foo/bar => deactivated
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)
}
}

有了这三个变量我们就可以轻松提取路由记录对应的路由钩子,包括离开,更新或者进入的钩子:

1
2
3
4
5
6
7
8
9
10
11
12
13
// 构建导航守卫函数队列
const queue: Array<?NavigationGuard> = [].concat(
// 组件beforeRouteLeave钩子
extractLeaveGuards(deactivated),
// 全局beforeEach
this.router.beforeHooks,
// 组件beforeRouteUpdate钩子
extractUpdateHooks(updated),
// 路由配置的组件beforeEnter钩子
activated.map(m => m.beforeEnter),
// 解析异步组件的钩子
resolveAsyncComponents(activated)
)

这个钩子函数数组会作为runQueue函数的参数进行调用:

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

这个方法从零开始遍历路由钩子,对于每个钩子都会作为fn函数参数调用,并且在第二个函数参数中递归调用进行下个钩子的执行逻辑,很明显第二个参数就类似我们钩子函数的nextfn是一个定义的迭代器来执行钩子函数:

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 iterator = (hook: NavigationGuard, next) => {
if (this.pending !== route) {
return abort();
}
try {
hook(route, current, (to: any) => {
if (to === false || isError(to)) {
// next(false) -> abort navigation, ensure current URL
// next(false)情况
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
// 在导航守卫中next重定向
abort();
if (typeof to === 'object' && to.replace) {
this.replace(to);
} else {
this.push(to);
}
} else {
// confirm transition and pass on the value
// 执行queue中的下一个钩子
next(to);
}
});
} catch (e) {
abort(e);
}
};

这个函数很简单,它先执行我们的路由钩子函数,然后判断传给next函数的参数,如果是false的情况直接调用abort函数中止导航,如果是字符串还要进行跳转到新的路径,最后才成功执行下一个钩子函数。在runQueue方法的钩子队列执行完后会执行下面的回调逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
const postEnterCbs = [];
const isValid = () => this.current === route;
// 提取组件的beforeRouteEnter
const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
// 全局beforeresolve
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) {
// 在route-view组件更新完后执行beforeRouteEnter钩子传给next函数的回调
this.router.app.$nextTick(() => {
postEnterCbs.forEach(cb => {
cb();
});
});
}
});

其中postEnterCbs是对beforeRouteEnter钩子中传给next回调参数的处理,它会在$nextTick视图渲染后执行,这个时候就能获取对应组件的实例对象。在执行完全局的beforeresolve钩子后,会先执行onComplete(route)方法执行成功的回调。在这个方法中会执行updateRoute来更新路由:

1
2
3
4
5
6
7
8
9
10
11
// 更新route
updateRoute (route: Route) {
const prev = this.current
this.current = route
// 修改app._route,通知视图更新
this.cb && this.cb(route)
// 执行全局beforeEach
this.router.afterHooks.forEach(hook => {
hook && hook(route, prev)
})
}

这个方法先更新this.current为新的路由对象,然后执行cb函数来设置注册路由的app的_route方法。它在init方法中进行定义:

1
2
3
4
5
6
7
8
9
10
11
// init
history.listen(route => {
this.apps.forEach((app) => {
app._route = route
})
})

// base.js
listen (cb: Function) {
this.cb = cb
}

因为在app根实例中对_route进行了响应式处理,所以重新设置就会触发它的setter,从而就会触发它的依赖进行更新,至于这些依赖其实就是用到route-view组件的实例的渲染watcher,然后这些实例就会重新执行render函数,根据最新的route对象来计算要渲染的路由组件,最后路由出口挂载最新的视图。在updateRoute方法最后,会执行全局的afterEach钩子。

url更新

在执行updateRoute方法后,还会执行传给transitionTo方法的成功回调。对于HashHistory对象,会执行pushHash方法来更新最新的路由路径:

1
2
3
4
5
6
7
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}

这个方法先判断浏览器是否支持pushState,支持的话通过getUrl方法得到完成的url并执行pushState设置。否则利用window.location.hash来设置浏览器url的hash:

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
// 根据path获取完成的url
function getUrl (path) {
const href = window.location.href
const i = href.indexOf('#')
const base = i >= 0 ? href.slice(0, i) : href
return `${base}#${path}`
}

export function pushState (url?: string, replace?: boolean) {
const history = window.history
try {
if (replace) {
// 传递之前的数据对象
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}

export function replaceState (url?: string) {
pushState(url, true)
}

这样当我们进行路由导航,浏览器的url就会更着变化。还有一种情况就是我们手动输入url,要监听响应的事件进行路由导航更新视图,对于hash类型它是在init中的setupListeners函数进行绑定:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
setupListeners () {
const router = this.router
// 监听url变化事件
window.addEventListener(
supportsPushState ? 'popstate' : 'hashchange',
() => {
const current = this.current
if (!ensureSlash()) {
return
}
// 根据最新的hash进行路由切换
this.transitionTo(getHash(), route => {
if (!supportsPushState) {
replaceHash(route.fullPath)
}
})
}
)
}

其中ensureSlash函数是处理不存在任何路径的情况,它会自动替换成#/

1
2
3
4
5
6
7
8
9
// 不存在hash的情况,默认为#/
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}

路由组件

router-view

Vue会在router-view组件中根据当前的路由对象渲染出正确的组件,那这一过程是怎么实现的。直接来看下这个组件的定义的渲染函数:

1
2
3
4
5
6
7
8
render(_, { props, children, parent, data }) { 
data.routerView = true
const h = parent.$createElement
// 默认为defualt
const name = props.name
const route = parent.$route
const cache = parent._routerViewCache || (parent._routerViewCache = {})
}

因为router-view是一个函数组件,它在渲染的过程中是不存在组件实例的。对比正常的组件渲染,它在组件占位节点的init钩子中新建组件实例,然后调用实例的render函数进行渲染。函数组件的render函数是在创建占位节点时候就直接执行了,也就是说当前的实例就是在route-view组件的环境,也就是传给函数组件render函数第二个参数的parent属性。

类比我们的例子,第一个route-view执行时的parent就是App组件的实例,第二个就是Foo组件的实例。

紧接着是计算当前router-view的深度,它是判断当前所在的组件的占位符vnode是否有routerView属性来进行判断:

1
2
3
4
5
6
7
8
9
10
11
12
let depth = 0;
while (parent && parent._routerRoot !== parent) {
// route-view所在组件环境的占位vnode
const vnodeData = parent.$vnode && parent.$vnode.data;
if (vnodeData) {
if (vnodeData.routerView) {
depth++;
}
}
parent = parent.$parent;
}
data.routerViewDepth = depth;

比如我们例子的第二个router-view所在组件是Foo,所以parent.$vnode就是Foo组件的占位vnode,因为它是挂载在第一个router-view的,所以它的routerViewtrue,于是depth为1;

在求出深度后,就可以在当前route对象的匹配路径中的matched属性得到渲染的组件对象components,然后根据具体的视图名称拿到对应的组件对象:

1
2
3
4
5
6
7
8
const matched = route.matched[depth]
// render empty node if no matched route
if (!matched) {
cache[name] = null
return h()
}

const component = cache[name] = matched.components[name]

然后在要挂载的组件占位vnode上的data设置registerRouteInstance函数,这个函数会在组件的beforeCreate钩子中调用,并且设置匹配到的路由记录的instances属性,这样才能在路由的beforeRouteEnter中拿到对应的组件实例:

1
2
3
4
5
6
7
8
9
10
data.registerRouteInstance = (vm, val) => {
// val could be undefined for unregistration
const current = matched.instances[name]
if (
(val && current !== vm) ||
(!val && current === vm)
) {
matched.instances[name] = val
}
}

最后返回将要挂载的组件的占位vnode:

1
return h(component, data, children)

因为我们在router-viewrender函数中用到了vm.$route属性,所以当前实例的渲染watcher会作为依赖进行收集,也就是说router-view组件所在的环境实例的render函数在路由切换时会重新执行。也就是为什么当我们进行路由导航时视图会更新的原因。

router-link组件是用来进行跳转的全局组件,它内部的实现原理也是调用了router.push路由方法进行导航。来看下render函数:

1
2
3
4
5
6
7
const router = this.$router
const current = this.$route
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)

首先调用router.resolve方法计算出新的路由对象:

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
resolve (
to: RawLocation,
current?: Route,
append?: boolean
): {
location: Location,
route: Route,
href: string,
// for backwards compat
normalizedTo: Location,
resolved: Route
} {
current = current || this.history.current
const location = normalizeLocation(
to,
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,
// for backwards compat
normalizedTo: location,
resolved: route
}
}

然后是一堆处理点击连接样式的。接着定义一个守卫函数guardEvent,它会在某些情况直接返回不进行导航:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 守卫函数
function guardEvent (e) {
// don't redirect with control keys
if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return
// don't redirect when preventDefault called
if (e.defaultPrevented) return
// don't redirect on right click
if (e.button !== undefined && e.button !== 0) return
// don't redirect if `target="_blank"`
if (e.currentTarget && e.currentTarget.getAttribute) {
const target = e.currentTarget.getAttribute('target')
if (/\b_blank\b/i.test(target)) return
}
// this may be a Weex event which doesn't have this method
if (e.preventDefault) {
e.preventDefault()
}
return true
}

然后定义连接点击的处理函数,它会根据配置判断调用push还是replace方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
const handler = e => {
if (guardEvent(e)) {
if (this.replace) {
router.replace(location, noop)
} else {
router.push(location, noop)
}
}
}

const on = { click: guardEvent };
if (Array.isArray(this.event)) {
// 处理配置的其他事件
this.event.forEach(e => {
on[e] = handler;
});
} else {
on[this.event] = handler;
}

对于tag不是a标签的情况,还要处理slot里面是否有a标签的情况,有的话把data附加到它身上,否则赋值在最外层上。最后返回tag创建的vnode:

1
return h(this.tag, data, this.$slots.default)

总结

到此,Vue-Router的源码就大致分析完了,其实里面有很多实现细节还没扣,比如怎么格式化一个location,怎么提取对应组件的路由钩子,滚动的处理等,但是这不影响路由变更到视图渲染的主流程。整个流程可以概括下图: