从官网文档开始
vue-router 官方文档中,基本用法如下
html:
// html
<router-link to="/foo">Go to Foo</router-link>
<router-link to="/bar">Go to Bar</router-link>
javascript:
// 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({
router
}).$mount('#app')
从以上代码我们大致可以猜到两点:
- VueRouter的初始化。首先把文档规定的路由配置集合(routes)传入构造类,创建出一个router对象。然后把router对象挂载到全局的Vue中。
- RouterLink组件和RouterView组件,两者应该配合完成了一些事情。目前还不知道,猜测可能是RouterLink组件传入要跳转的路由信息,然后通过一个类似全局对象和RouterView通信?
这里就引出了三个个问题:
- VueRouter对象在构造过程中,有那些处理?
- RouterLink组件和RouterView组件是如何通信的?
- 点击RouterLink组件过程中发生了什么?
那么我们就从这两个问题开始我们源码探索之旅。我选取的版本是目前最新的3.4.8
VueRouter构造过程
首先我们也按照官方文档步骤,先从VueRouter构造函数入手,代码如下:
// VueRouter的constructor
constructor (options: RouterOptions = {}) {
// ...省略,一些属性设置
// 创建Matcher
this.matcher = createMatcher(options.routes || [], this)
let mode = options.mode || 'hash'
// ...一系列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:
// ...
}
}
眯起眼睛看的话,以上过程主要是创建了一个Matcher对象和History对象。
创建Matcher对象
createMatcher,看名字就知道是用来创建的Matcher对象的,代码如下
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 {
//...暂时省略
}
function redirect (
record: RouteRecord,
location: Location
): Route {
//...暂时省略
}
function alias (
record: RouteRecord,
location: Location,
matchAs: string
): Route {
//...暂时省略
}
function _createRoute (
record: ?RouteRecord,
location: Location,
redirectedFrom?: Location
): Route {
//...暂时省略
}
return {
match,
addRoutes
}
}
其中的addRoutes函数可能就是文档上提到过的动态添加路由的api,它也是在createMatcher函数中创建的。先来看下createMatcher执行过程中的调用了createRouteMap函数
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)
})
//... 省略
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap对传入的routes参数进行了遍历地调用addRouteRecord。再进入addRouteRecord函数中看一下吧
function addRouteRecord (
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
const record: RouteRecord = {
// ...省略其他属性
components: route.components || { default: route.component }
}
// 遍历孩子节点
if (route.children) {
route.children.forEach(child => {
const childMatchAs = 'XXXX'
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 原地在pathList和pathMap添加数据
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 处理alias
if (route.alias !== undefined) {
const aliases = Array.isArray(route.alias) ? route.alias : [route.alias]
for (let i = 0; i < aliases.length; ++i) {
const alias = aliases[i]
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
// 处理有name属性的情况
if (name) {
if (!nameMap[name]) {
nameMap[name] = record
}
}
}
这里递归调用addRouteRecord。在遍历了当前躺节点的孩子后,又对当前躺传入的参数(与RouteRecord关联的数据集合)进行了原地修改。到此发现没有可以往下进行的地方了,创建Matcher的过程结束。
小结
创建Matcher对象的过程如下图所示
其中涉及到数据类型有Matcher、RouteConfig、RouteRecord。
Matcher类型有两个方法,match和addRoutes
type Matcher = {
match: (raw: RawLocation, current?: Route, redirectedFrom?: Location) => Route;
addRoutes: (routes: Array<RouteConfig>) => void;
}
RouteConfig就是官方文档上规定的,要传入的routes对象
const routes = [
{ path: '/foo', component: Foo },
{ path: '/bar', component: Bar }
]
RouteRecord是从RouteConfig转换过来的,是在组件内部使用的路由数据类型。
总结Matcher的创建过程,就是根据传入的路由配置表信息,生成了内部使用内部使用的路由表。得到了一个matcher对象,它有两个方法,从数据类型上推测这两个方法的作用:
- addRoutes方法:动态对路由配置表进行添加,
- match方法:根据传入参数,获取Route对象
创建History对象
创建History对象过程还是比较简单的,VueRouter根据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:
// ...
}
以HashHistory为例,它继承了History类
class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
//...
}
// ...
}
在看History类,注意这个START变量,来看看它具体是什么。
export class History {
// ...
constructor (router: Router, base: ?string) {
this.router = router
this.base = normalizeBase(base) // 确定basePath
// start with a route object that stands for "nowhere"
this.current = START
// this.xxx = []等等
}
//..
}
START变量是在const START = createRoute(null, { path: '/'})这里创建的,createRoute函数看名字,应该是创建供内部使用的Route路由信息的对象。
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对象,到这里History的构造过程就分析完毕了。总结一下,目前这个过程主要是创建了Route对象,保存传入的事件到队列中。
路由对象注入Vue中
上面的过程完毕后,生成了router对象后,接下来就是把路由注入到Vue实例中
const app = new Vue({
router
}).$mount('#app')
通过学习Vue插件,我们知道上面的代码是通过全局混入来添加组件选项的。代码在install.js中
if (install.installed && _Vue === Vue) return
install.installed = true
_Vue = Vue
const isDef = v => v !== undefined
const registerInstance = (vm, callVal) => {
// vm的父节点的选项中有registerRouteInstance的话,就执行registerRouteInstance(vm, callVal)
}
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
this._router.init(this)
// 访问_route,就是访问router.history.current
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
registerInstance(this, this)
},
destroyed () {
registerInstance(this)
}
})
// Vue原型中挂载$router
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
// Vue原型中挂载$route
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// 注册RouterView和RouterLink组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
在beforeCreate回调中,做了两件事情,一个是设置了对Vue根对象设置了router和route属性,并且分别通过Object.defineProperty在Vue的原型中挂载成为route。this._router.init(this)里面是history对象中,注册了一些公共事件。
最后还对RouterView和RouterLink组件进行了注册。下面就这两个组件进行分析
RouterLink组件
RouterLink组件代码如下:
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
// ...
event: {
type: eventTypes,
default: 'click'
}
},
render (h: Function) {
…
}
}
只有一个render函数,处理逻辑应该就放在了这里。
// RouterLink的render函数
render (h: Function) {
const router = this.$router
const current = this.$route
// 这里调用的router的resovle方法有什么作用还不得而知,不过看返回值有location、route,href的话,也能猜到大概了。
const { location, route, href } = router.resolve(
this.to,
current,
this.append
)
/*
省略部分是对class进行处理
*/
// 处理事件选项
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
}
// data是h函数要传入的选项
const data: any = { class: classes } // 之前省略的class处理结果
/*
处理插槽
*/
const scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default &&
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})
if (scopedSlot) {
if (scopedSlot.length === 1) {
return scopedSlot[0]
} else if (scopedSlot.length > 1 || !scopedSlot.length) {
return scopedSlot.length === 0 ? h() : h('span', {}, scopedSlot)
}
}
/*
以下代码是合并data选项并传入h函数中,
其中会有a标签处理:如果当前标签是a标签或者插槽里面有a标签,就以找出他来,并把a标签的选项混入到选项中
*/
if (this.tag === 'a') {
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
} else {
// find the first <a> child and apply listener and href
const a = findAnchor(this.$slots.default)
if (a) {
// in case the <a> is a static node
a.isStatic = false
const aData = (a.data = extend({}, a.data))
aData.on = aData.on || {}
// transform existing events in both objects into arrays so we can push later
for (const event in aData.on) {
const handler = aData.on[event]
if (event in on) {
aData.on[event] = Array.isArray(handler) ? handler : [handler]
}
}
// append new listeners for router-link
for (const event in on) {
if (event in aData.on) {
// on[event] is always a function
aData.on[event].push(on[event])
} else {
aData.on[event] = handler
}
}
const aAttrs = (a.data.attrs = extend({}, a.data.attrs))
aAttrs.href = href
aAttrs['aria-current'] = ariaCurrentValue
} else {
// 没有a标签的情况,比如自定义的组件
data.on = on
}
}
return h(this.tag, data, this.$slots.default)
}
整理上面的代码,主要做的工作是组织h函数用到的data选项参数和插槽。在data选项添加了公共的事件处理,插槽部分混入了前面得到的route等对象。注意在这里调用了router.resovle方法,包括添加的事件函数里面也调用了router.push/replace方法。RouterLink组件可能就要通过这里与Router对象产生联系了。我们先看router.resovle方法吧。
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) // Location类型
const route = this.match(location, current) // 这里的this是Router对象,返回Route类型
// ...省略
return {
location,
route
// ...省略
}
}
回想创建Matcher对象这个部分,这里的router.resovle方法就是调用了我们在VueRouter构造过程中创建了matcher对象中的match方法。
function match (
raw: RawLocation, // raw就是api文档中要传入的to参数,<router-link to="/foo">Go to Foo</router-link>
currentRoute?: Route,
redirectedFrom?: Location
): Route {
const location = normalizeLocation(raw, currentRoute, false, router) // 得到将来要跳转的相关信息
const { name } = location
/*
下面的nameMap,pathList和pathMap都是之前createMatcher已经创建好的
*/
// 有name的情况
if (name) {
const record = nameMap[name]
if (!record) return _createRoute(null, location)
const paramNames = record.regex.keys.filter(key => !key.optional).map(key => key.name) // regex解析参考path-to-regexp库
if (typeof location.params !== 'object') {
location.params = {}
}
// 把location.params没有但currentRoute.params有的属性,赋值给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)
} else if (location.path) {
location.params = {}
// 遍历保存的路由配置表数据,如果record的regex正则如果能与location.path匹配
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)
}
match方法传入了将来跳转的to对象,返回一个Route类型的对象。可以看代码中总会调用_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)
}
_createRoute会根据情况选择执行redirect或者alias,这两个函数最后都会调用createRoute方法。这个方法前面的创建History对象章节已经提到过,是用来创建Route对象的。
至此RouterLink组件大体上分析完毕,做的工作有:
- 添加公共的事件绑定(主要是click事件,绑定的函数会调用router的replace/push方法)
- 生成将来要跳转的路由route
- 处理插槽slot
RouterView组件
下面我们分析,RouterView组件,它是一个函数式组件,接收的props只有一个name
export default {
name: 'RouterView',
functional: true,
props: {
name: {
type: String,
default: 'default'
}
},
render (_, { props, children, parent, data }) {
...
}
}
接下来看看render中做了什么
// RouterView render方法
render (_, { props, children, parent, data }) {
// 使用父类的$createElement方法作为渲染函数,这样组件就可以渲染传入的具名插槽
const h = parent.$createElement
const name = props.name
const route = parent.$route // 这里的$route触发了install.js中的 this._routerRoot._route =》 this._router.history.current
const cache = parent._routerViewCache || (parent._routerViewCache = {})
/*
检查当前嵌套路由组件相对根组件嵌套的深度,同时检测dom树中是否存在inactive同时已经keep-live状态的组件
如果存在就渲染它
*/
// ...省略代码中获取了嵌套路径片段的路由记录的depth
const matched = route.matched[depth]
const component = matched && matched.components[name]
// 没有路由记录,或者没有传组件,直接渲染,并且清空缓存
if (!matched || !component) {
cache[name] = null
return h()
}
// 如果有嵌套路由:缓存组件
cache[name] = { component }
/*
省略代码是添加了一些hook函数
*/
const configProps = matched.props && matched.props[name]
// 把路由记录matched中的props属性合并选项对象data中
if (configProps) {
extend(cache[name], {
route,
configProps
})
fillPropsinData(component, data, route, configProps)
}
return h(component, data, children)
}
总结下RouterView在render过程中的工作:
- 检查是否有嵌套路由的情况,或者没有component的情况
- 不存在,直接渲染
- 存在,把组件缓存在当前的父级组件对象
- 把路由记录matched中的props属性合并选项对象data中
- 组件来自全局注册的route也就router.history.current来动态的渲染组件。
触发$router的api
根据上面的分析,我们猜测,通过点击RouterLink组件,触发$router的方法,从而改变绑定history上的数据,这就整体的流程就完成了。下面就分析一下,事件绑定在RouterLink组件中,回顾对应小结并进行代码追踪发现,点击事件总会触发router.push/replace这个两个方法,这两个方法又都调用了transitionTo方法。
// transitionTo
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route = this.router.match(location, this.current)
const prev = this.current
this.confirmTransition(
route,
() => {
this.updateRoute(route)
// ...onComplete
},
err => {
//...onAbort
}
)
}
transitionTo里,调用了router.match方法,得到要跳转的route对象。传给confirmTransition方法里的回调函数中执行了this.updateRoute(route)。这里执行了$route的更新。
updateRoute (route: Route) {
this.current = route
this.cb && this.cb(route)
}
我们终于找到了$route更新的地方!对于vue-router的整体分析可以暂时告一段落了。
后面还调用了confirmTransition方法
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
const current = this.current // Route对象,也是$route最后要访问的对象
this.pending = route
const abort = err => {
// ...触发的话,会把错误队列errorCbs里的保存的函数,或者console.error(err),有onAbort回调就执行回调
}
const lastRouteIndex = route.matched.length - 1
const lastCurrentIndex = current.matched.length - 1
// 判断要改变的route和当前的route是否『相等』
if (
isSameRoute(route, current) &&
// 处理路由动态添加的情况?
lastRouteIndex === lastCurrentIndex &&
route.matched[lastRouteIndex] === current.matched[lastCurrentIndex]
) {
// createNavigationDuplicatedError ,看名字应该是报一个导航重复的错误
return abort(createNavigationDuplicatedError(current, route))
}
const { updated, deactivated, activated } = resolveQueue(
this.current.matched,
route.matched
)
// 定义导航守卫
const queue: Array<?NavigationGuard> = [].concat(
// ......
)
// 定义了一个迭代器
const iterator = (hook: NavigationGuard, next) => {
}
// 执行一系列生命周期的hook
runQueue(queue, iterator, () => {
})
}
到这里发现,点击事件的执行完成了。
总结下,尝试解答一下开篇提到的问题:
1. VueRouter对象在构造过程中,有那些处理?
- 创建matcher对象。这个对象作用是匹配传入的路径参数(RawLocation),生成对应的路由信息对象(route),我们在vue组件中访问的$route对象,实际上都是通过这个方法获得的。经过处理的路由配置信息也是保存在了matcher关联的闭包中。
- 创建history对象,这个对象负责了事件队列处理,为router提供api支持。很多底层方法都是放在了这里。如果想看导航守卫、文档上的api源码的话,可以看下这里。
2. RouterLink组件和RouterView组件是如何通信的?
VueRouter在Vue中注册的时候,会在Vue原型中添加route的get访问方法。router.history.current。RouterLink通过传入的参数(例如,to="/foo"),调用router.match后获取下一跳的路由对象route。在VueRouter注册过程中,对route变化对触发VueRouterView组件的render方法,从而进行后续的操作。
3. 点击RouterLink组件过程中发生了什么?
点击RouterLink组件的处理过程中,会触发router.push/replace方法,更新$route值。之后会把注册的函数队列按照顺序执行。
总结
整个VueRouter的源码分析就结束了,其中有很多值得学习的地方。比如在VueRouter的constructor方法中,对于History的创建采用了工厂模式,用来应对不同的路由模式。创建matcher的地方对应闭包的使用同样很好。还有对于router和route的使用了Object.defineProperty挂载属性,这样访问代码能少写很多。在history里对于事件的处理中,使用到了事件队列和迭代器,优雅的实现了导航守卫以及相关的事件处理代码。
参考
- path的路径解析用到了path-to-regexp
- Vue源码-Vue-Router