date: 2021-11-29 19:03:44
tags: vue-router 源码解析
author: coder@mc
定场诗
人人尽说清闲好,谁肯逢闲闲此身。
不是逢闲闲不得,清闲岂是等闲人。
版本说明
此博客主要是针对 vue-router@3.5.2做一次源码解析,主要研究以下几点:
install函数;- 初始化过程,即
VueRouter构造函数的实现; - 路由跳转流程;
<router-view>和router-link。
思维导图
Demo
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter)
import VueRouter from 'vue-router';
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({
router
}).$mount('#app')
上面的代码是vue-router官方例子,想要了解源码,得从两方面入手,一个是作为导入的插件被Vue.use注册时,instll函数具体实现过程;另一个是new VueRouter() 时具体做了哪些事,下面我们先看第一个:
一、install函数
我们都知道Vue.use(VueRouter)注册时必须要存在install函数,而import VueRouter from 'vue-router'导入时的入口文件是src/index.js,打开文件后会有一句:
VueRouter.install = install
可以看到install函数赋值给VueRouter,所以可以被vue.use()作为插件安装在Vue中,install函数源码位于src/install.js:
import View from './components/view'
import Link from './components/link'
export let _Vue
/**
* 1. 标记 vue-router 已经被注册过了
* 2. 为了每个组件都能使用 router, 则通过 Vue.mixin 来创建公用数据
* 3. Vue.prototype.defineProperty 劫持,使得能通过 this.$router 或者 this.$route 访问
* 4. 定义 router-view 和 router-link 组件
*
* @param {*} Vue
*/
export function install (Vue) {
// 表示 vue-router 已安装,防止重复安装
if (install.installed && _Vue === Vue) return
install.installed = true
// 把 Vue 存起来并 export 供其它文件使用
_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)
}
}
// 注册一个全局的 mixin
Vue.mixin({
// 生命周期 beforeCerate 钩子函数
beforeCreate () {
// 初始化
if (isDef(this.$options.router)) {
this._routerRoot = this
this._router = this.$options.router
// 调用 router.init()
this._router.init(this)
// 把 _router 变为响应式
Vue.util.defineReactive(this, '_route', this._router.history.current)
} else {
// 如果已经初始化,继承父组件的 _routerRoot
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
// 注册实例,实际上是挂载 <router-view></router-view>
registerInstance(this, this)
},
destroyed () {
// 离开时卸载
registerInstance(this)
}
})
// this.$router/$route
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
// <router-view> 和 <router-link> 组件
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
// 利用 Vue 的合并策略新增几个相关的生命周期
const strats = Vue.config.optionMergeStrategies
// use the same hook merging strategy for route hooks
strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created
}
该函数做了如下几件事:
- 通过给
install函数上赋值静态属性installed,标记vue-router已经被注册过了,防止重复注册; - 用了全局混入,使每个组件都能使用
router,具体通过给每个组件添加_routerRoot属性,这个属性的_router其实就是new VueRouter()实例化后的对象,实例化过程看第二部分; - 通过
Object.defineProperty方法劫持组件实例上$router和$route属性,使得每个组件可以通过this.$router和this.$route访问到router实例和router.history.current; - 注册
<router-view>和<router-link>组件; - 利用 Vue 的合并策略新增几个相关的生命周期,包括
beforeRouteEnter、beforeRouteLeave、beforeRouteUpdate、created。
二、初始化过程
export default class VueRouter {
constructor (options: RouterOptions = {}) {
// 根 Vue 实例
this.app = null
// 存在多实例的话,则保存
this.apps = []
// 传入的配置
this.options = options
// 存放已注册的导航守卫
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// 创建 matcher, 返回 match, addRoute, getRoutes, addRoutes 四个函数
this.matcher = createMatcher(options.routes || [], this)
// 默认是 hash 模式
let mode = options.mode || 'hash'
// 如果使用了 history 模式,但不支持 pushState 也需回退到 hash
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
if (this.fallback) {
mode = 'hash'
}
// 非浏览器环境(SSR),则使用 abstract
if (!inBrowser) {
mode = 'abstract'
}
this.mode = mode
// 根据不同的 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}`)
}
}
}
...
}
可以看到在new VueRouter(options)时,大致可以分为三部分:
第一部分:初始化一些内置属性,比如
options配置,导航守卫数组等;第二部分:创建
matcher,返回一个对象,里面有4个方法match,addRoute,getRoutes,addRoutes;第三部分:根据
mode构建不同的的history
其中第一部分初始化一些内置属性没什么可讲解的,主要看第二部分和第三部分,先看创建matcher。
1. 创建macther
// 创建 matcher, 返回 match, addRoute, getRoutes, addRoutes 四个函数
this.matcher = createMatcher(options.routes || [], this)
从字面意思去解读:createMatcher是个函数,并传入两个参数routes ||[] 和this。createMatcher函数的源码位于src/create-matcher.js:
export function createMatcher (
routes: Array<RouteConfig>,
router: VueRouter
): Matcher {
// pathList: path 的集合 [path1, path2, ...],
// pathMap: path作为键的映射对象 {path1: record},
// 当 route 里面有 name 则,将 name 作为键, record 作为值存储到 nameMap 中
const { pathList, pathMap, nameMap } = createRouteMap(routes)
function addRoutes (routes) { ... }
function addRoute (parentOrRoute, route) { ... }
function match (raw, currentRoute, redirectedFrom) {...}
function getRoutes () {...}
...
return {
match,
addRoute,
getRoutes,
addRoutes
}
}
上面代码做了两件事,一是通过调用createRouteMap(routes)函数,返回了一个对象并解构里面的属性定义了三个变量pathList,pathMap和nameMap;二是返回了四个函数。第二步暂时还不看,先看createRouteMap函数,该函数源码位于src/create-route-map.js:
/**
* 1. 处理配置项 routes 里面的每一项,
* 将 path 放到 pathList 数组中,将 path 作为 pathMap 的键,record({path: 'xx', name: '', ...}) 做值
* 2. 确保 pathList 中通配符在末尾
* 3. 非嵌套路由 path !== '' 的情况下首字符没有包含斜杠字符,会报警告
* 4. 返回 pathList,pathMap,nameMap
* @param {*} routes 路由配置
* @param {*} oldPathList [path1. path2, ...]
* @param {*} oldPathMap [{path1: {path: path1, ...}}]
* @param {*} oldNameMap
* @param {*} parentRoute
*/
export function createRouteMap(
routes: Array<RouteConfig>,
oldPathList?: Array<string>,
oldPathMap?: Dictionary<RouteRecord>,
oldNameMap?: Dictionary<RouteRecord>,
parentRoute?: RouteRecord
): {
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>
} {
// the path list is used to control path matching priority
const pathList: Array<string> = oldPathList || []
// $flow-disable-line
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
// $flow-disable-line
const nameMap: Dictionary<RouteRecord> = oldNameMap || Object.create(null)
// 遍历每项 route, 生成三张表
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// ensure wildcard routes are always at the end
// 确保通配符始终在末尾
for (let i = 0, l = pathList.length; i < l; i++) {
if (pathList[i] === '*') {
pathList.push(pathList.splice(i, 1)[0])
l--
i--
}
}
// 警告:非嵌套路由首字符包含斜杠字符
if (process.env.NODE_ENV === 'development') {
// warn if routes do not include leading slashes
const found = pathList
// check for missing leading slash
.filter(path => path && path.charAt(0) !== '*' && path.charAt(0) !== '/')
if (found.length > 0) {
const pathNames = found.map(path => `- ${path}`).join('\n')
// 非嵌套路由必须包含斜杠字符
warn(false, `Non-nested routes must include a leading slash character. Fix the following routes: \n${pathNames}`)
}
}
return {
pathList,
pathMap,
nameMap
}
}
createRouteMap函数可以分为四部分去阅读:
- 定义了三个变量,通过遍历传入的
routes,调用addRouteRecord函数处理配置项routes里面的每一项,将path放到pathList数组中,将path作为pathMap的键,record({path: 'xx', name: '', ...})做值; - 确保通配符始终在末尾;
- 非嵌套路由 path !== '' 的情况下首字符没有包含斜杠字符,会报警告;
- 返回
pathList,pathMap,nameMap三张表。
需要关注的是addRouteRecord函数怎样处理routes中的每项,源码位于当前文件里面的addRouteRecord函数:
/**
* 1. 检查是否有配置 pathToRegexpOptions 选项,这个属性值是路由高级匹配模式 (path-to-regex) 的参数
* 2. 调用 normalizePath 将 path 标准化,这里会将子路由的 path 和 父路由的 path 拼接在一起
* 3. 处理 caseSensitive 参数,这是 path-to-regexp 中的参数
* 4. 声明一个 RouteRecord
* 5. 该路由存在子路由,递归调用 addRouteRecord 添加路由记录
* 6. 将 record 存入 pathList,将这条记录以 path 作为 key 存入 pathMap
* 7. 如果存在 alias,则用 alias 作为 path 再添加一条路由记录
* 8. 如果存在 name,则用 name 作为 key 存入到 nameMap
* @param {*} pathList
* @param {*} pathMap
* @param {*} nameMap
* @param {*} route
* @param {*} parent
* @param {*} matchAs
*/
function addRouteRecord(
pathList: Array<string>,
pathMap: Dictionary<RouteRecord>,
nameMap: Dictionary<RouteRecord>,
route: RouteConfig,
parent?: RouteRecord,
matchAs?: string
) {
const { path, name } = route
// 校验 path,route 的 component 属性
if (process.env.NODE_ENV !== 'production') {
// path 是必须传入的
assert(path != null, `"path" is required in a route configuration.`)
// component 不能是字符串,必须是组件
assert(
typeof route.component !== 'string',
`route config "component" for path: ${String(
path || name
)} cannot be a ` + `string id. Use an actual component instead.`
)
// path 是 ASCLL 字符
warn(
// eslint-disable-next-line no-control-regex
!/[^\u0000-\u007F]+/.test(path),
`Route with path "${path}" contains unencoded characters, make sure ` +
`your path is correctly encoded before passing it to the router. Use ` +
`encodeURI to encode static segments of your path.`
)
}
// 编译正则的选项
const pathToRegexpOptions: PathToRegexpOptions =
route.pathToRegexpOptions || {}
// 调用 normalizePath 将 path 标准化,
// 如果存在父路由,这里会将子路由的 path 和 父路由的 path 拼接在一起
const normalizedPath = normalizePath(path, parent, pathToRegexpOptions.strict)
// 处理 caseSensitive 参数,这是 path-to-regexp 中的参数
if (typeof route.caseSensitive === 'boolean') {
pathToRegexpOptions.sensitive = route.caseSensitive
}
// 声明一个 RouteRecord
const record: RouteRecord = {
path: normalizedPath,
// 用于匹配该路由的正则
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
// 该路由对应的组件,这里与 <router-view> 的 name 有关联
components: route.components || { default: route.component },
// 路由别名
alias: route.alias
? typeof route.alias === 'string'
? [route.alias]
: route.alias
: [],
instances: {},
enteredCbs: {},
name,
parent,
matchAs,
redirect: route.redirect,
beforeEnter: route.beforeEnter, // 导航守卫
meta: route.meta || {}, // 元信息
props:
route.props == null
? {}
: route.components
? route.props
: { default: route.props }
}
// 该路由存在子路由,递归调用 addRouteRecord 添加路由记录
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,递归执行 addRouteRecord
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
}
// 将 record 存入 pathList,将这条记录以 path 作为 key 存入 pathMap
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
// 如果存在 alias,则用 alias 作为 path 再添加一条路由记录
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]
// alias 不能和 path 重名
if (process.env.NODE_ENV !== 'production' && alias === path) {
warn(
false,
`Found an alias with the same value as the path: "${path}". You have to remove that alias. It will be ignored in development.`
)
// skip in dev to make it work
continue
}
const aliasRoute = {
path: alias,
children: route.children
}
addRouteRecord(
pathList,
pathMap,
nameMap,
aliasRoute,
parent,
record.path || '/' // matchAs
)
}
}
// 如果存在 name,则用 name 作为 key 存入到 nameMap
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}" }`
)
}
}
}
代码很长,前面经过一系列处理生成一个RouteRecord 变量,将 RouteRecord.path 存入 pathList,将这条记录以 path 作为 key存入 pathMap ;如果存在 name,则用 name 作为 key 存入到 nameMap;如果routes里存在children属性,则递归遍历该函数。
总结:createMatcher函数其实就是里面生成了三张表(注意:三张表作为全局变量,供里面方法调用,并未作为私有属性),并定义了一些方法作为返回值,放到router实例的matcher属性上。
2. 构建history
根据不同的mode构建不同的history属性,这里以mode为hash为例:
this.history = new HashHistory(this, options.base, this.fallback)
可以看到,通过new HashHistory() 实例化了一个hsitory对象,并传入三个参数this,options.base和this.fallback,前两个参数可以理解,this.fallback是什么呢?答案如下:
// 如果使用了 history 模式,但不支持 pushState 也需回退到 hash
this.fallback =
mode === 'history' && !supportsPushState && options.fallback !== false
this.fallback是一个布尔值,如果使用了 history 模式,但不支持 pushState,并且options.fallback也不为false时才为true。接下来具体看HashHistory构造函数初始化过程,该源码位于src/history/hash.js:
export class HashHistory extends History {
constructor (router: Router, base: ?string, fallback: boolean) {
super(router, base)
// 不支持 history 模式,支持 hash 模式,检查是否因为回退而使用 hash 模式,如果是的话则调用 checkFallback 检查它的返回值
// 如果为 true,则不调用 ensureSlash()
if (fallback && checkFallback(this.base)) {
return
}
// 判断首字符是否存在 /,如果不是则要重定向以 / 开头的 URL
ensureSlash()
}
...
}
HashHistory构造函数做了三件事,一是通过super关键字调用父类构造函数History;二是,检查传入的fallback,如果为true则直接返回;三是判断首字符是否存在 /,如果不是则要重定向以 / 开头的 URL。父类构造函数History的源码位于src/history/base.js:
export class History {
constructor (router: Router, base: ?string) {
// vueRouter 实例
this.router = router
// 序列化后的 base(首字母 "/" 最后一个字符不是 "/")
this.base = normalizeBase(base)
// start with a route object that stands for "nowhere"
this.current = START
this.pending = null
this.ready = false
this.readyCbs = []
this.readyErrorCbs = []
this.errorCbs = []
this.listeners = []
}
...
}
ensureSlash方法源码如下:
// 如果首字符是 / 返回 true,否则返回 false
function ensureSlash (): boolean {
const path = getHash()
if (path.charAt(0) === '/') {
return true
}
replaceHash('/' + path)
return false
}
三、路由跳转
路由跳转这里准备分为两部分了解:
第一部分:
url输入;第二部分:
TransitionTo方法;第三部分:通过使用
this.$router.push(xx)和this.$router.replace(xx)跳转都干了什么。
1. url 输入
输入网址时,展示的是routes里对应的组件,跳转组件必然会经历生命周期函数。回顾一下,在安装插件时,会混入beforeCreate生命周期函数,每个组件都会走这一生命周期函数,里面有一句:
// 调用 router.init()
this._router.init(this)
可以看到会调用router的init方法,并将当前组件作为参数传递过去,init是VueRouter的原型方法,位于src/index.js:
/**
* 1. 开发环境中确保使用 Vue.use 安装插件
* 2. 将 vue 实例存入到 apps 中
* 3. 当前实例销毁时需要在 apps 中移除
* 4. 根据当前路由做路由跳转
* 5. 监听路由变化给实例设置 _route 属性,以便通过 this.$route 获取
* @param {*} app Vue 实例
*/
init (app: any /* Vue component instance */) {
// 开发环境下检查是否已安装
process.env.NODE_ENV !== 'production' &&
assert(
install.installed,
`not installed. Make sure to call \`Vue.use(VueRouter)\` ` +
`before creating root instance.`
)
// 保存当前 app 实例
this.apps.push(app)
// 当前 app 销毁时需要在 apps 中移除
app.$once('hook:destroyed', () => {
// clean out app from this.apps array once destroyed
const index = this.apps.indexOf(app)
if (index > -1) this.apps.splice(index, 1)
// ensure we still have a main app or null if no apps
// we do not release the router so it can be reused
if (this.app === app) this.app = this.apps[0] || null
if (!this.app) this.history.teardown()
})
// 防止重复调用
if (this.app) {
return
}
// 根 Vue 实例
this.app = app
// 当前的 history,由之前的 new Router 时根据不同 mode 来创建
const history = this.history
// 根据当前路由位置做路由跳转
if (history instanceof HTML5History || history instanceof HashHistory) {
const handleInitialScroll = routeOrError => {
const from = history.current
const expectScroll = this.options.scrollBehavior
const supportsScroll = supportsPushState && expectScroll
if (supportsScroll && 'fullPath' in routeOrError) {
handleScroll(this, routeOrError, from, false)
}
}
const setupListeners = routeOrError => {
history.setupListeners()
handleInitialScroll(routeOrError)
}
// 初始化切换路由的方法
history.transitionTo(
history.getCurrentLocation(),
setupListeners,
setupListeners
)
}
// 监听路由变化,在所有 app 实例中设置当前路由
// 所以可以通过 this.$route 拿到当前路由
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
着重需要关注的是路由跳转方法history.transitionTo()方法,这是输入网址时路由跳转的方法。可以看下传递的第一个参数history.getCurrentLocation():
getCurrentLocation () {
return getHash()
}
/**
* 截取 hash 后面的字符,例如:basePath/#/path => /path
*/
export function getHash (): string {
let href = window.location.href
const index = href.indexOf('#')
if (index < 0) return ''
href = href.slice(index + 1)
return href
}
其实就是当前路由。
2. transitionTo方法
由于init方法和push/replace方法里都调用了该方法,所以放在同层讲解,该方法位于src/history/base.js:
transitionTo (
location: RawLocation,
onComplete?: Function,
onAbort?: Function
) {
let route
// 这里要 try 一下是因为 match 方法内部会在有 redirect 属性时调用它
// 但用户提供的 redirect 方法可能会报错,所以这里需要捕获到错误的回调方法
try {
route = this.router.match(location, this.current)
} catch (e) {
this.errorCbs.forEach(cb => {
cb(e)
})
// 依然要抛出异常,让用户得知
throw e
}
// 记录之前的路由,后面会用到
const prev = this.current
// 切换路由的真正方法
this.confirmTransition(
// 传入准备切换的路由
route,
// 切换之后的回调
() => {
...
},
// 发生错误的回调
err => {
...
}
)
}
这一函数其实就是做了两件事,一是通过调用this.router.match(location, this.current)方法来生成,route变量;二是调用真正的切换路由方法confirmTransition。
2.1 match方法
// 实际上是调用 matcher 的 match 方法
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
可以看到实际上是调用 matcher 的 match 方法,还记得matcher.mactch方法在哪吗?上面提到过match方法,该方法位于src/create-matcher.js:
/**
* 1. 将待切换的路由转换成一个标准的 Location 对象
* 2. 判断是否存在 name 属性,若存在,则继承父路由的 params 属性,并将 path 和 params 合并为 URL 创建路由对象
* 3. name 不存在,判断是否存在 path 属性,遍历 pathList 路由表,找到对应的记录,并创建路由对象,否则,创建一个空路由对象
* 4. 上面两种情况都不存在,则创建一个空路由对象
* @param {*} raw 待切换的路由
* @param {*} currentRoute 当前路由
* @param {*} redirectedFrom 使用重定向方式切换时才会传入
*/
function match (
// 待切换路由,取值为 字符串 或 Location对象
raw: RawLocation,
// 当前的路由
currentRoute?: Route,
// 使用重定向方式切换时才会传入
redirectedFrom?: Location
): Route {
// 将待切换的路由转换成一个标准的 Location 对象
// 比如:path 补全,合并 params
const location = normalizeLocation(raw, currentRoute, false, router)
// 待切换路由的 name
const { name } = location
if (name) {
// 有 name 的属性,直接通过 nameMap 获取,根本无需遍历,非常高效
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)
// 获取可以从父路由中继承的 param 参数
const paramNames = record.regex.keys
.filter(key => !key.optional)
.map(key => key.name)
// params 需要为对象
if (typeof location.params !== 'object') {
location.params = {}
}
// 继承父路由的 param 参数
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]
}
}
}
// 将 path 和 params 合并为 URL
location.path = fillParams(record.path, location.params, `named route "${name}"`)
// 创建路由记录
return _createRoute(record, location, redirectedFrom)
} else if (location.path) {
// 如果是通过 path 跳转,则需要通过遍历 pathList 匹配对应的路由
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)
}
该方法最终都会执行_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方法,该方法位于src/util/route.js:
/**
* 创建 route 对象,不可修改的对象
* @param {*} record
* @param {*} location
* @param {*} redirectedFrom
* @param {*} router
*/
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不可修改对象,包含name,path,meta,hash等属性。
2.2 confirmTransition方法
confirmTransition (route: Route, onComplete: Function, onAbort?: Function) {
...
}
-
获取到 跳转前的路由(from) 与 待跳转的路由(to)
// 跳转前的路由 (from) const current = this.current // 待跳转的路由 (to) this.pending = route -
错误的回调
// 错误的回调 const abort = err => { if (!isNavigationFailure(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) } -
判断是否是相同的路由
const lastRouteIndex = route.matched.length - 1 const lastCurrentIndex = current.matched.length - 1 // 判断是否是相同路径,例如:同一路由点击多次 if ( isSameRoute(route, current) && lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { // 依旧切换 this.ensureURL() // 报一个重复导航的错误 return abort(createNavigationDuplicatedError(current, route)) } -
导航流程
// 通过 from 和 to 的 matched 数组拿到新增,更新,销毁的部分,以便执行组件的生命周期 const { updated, deactivated, activated } = resolveQueue( this.current.matched, route.matched ) // 一个队列,里面存放着各种组件的生命周期和导航守卫 // 注意:这里只是完整的解析流程 2~6 步 const queue: Array<?NavigationGuard> = [].concat( // 调用此次失活的部分组件的 beforeRouteLeave extractLeaveGuards(deactivated), // 调用全局的 before 钩子 this.router.beforeHooks, // 调用此次更新的部分组件的 beforeRouteUpdate extractUpdateHooks(updated), // 调用此次激活的路由配置 beforeEnter 钩子函数 activated.map(m => m.beforeEnter), // 解析异步路由组件 resolveAsyncComponents(activated) ) // 迭代器,每次执行一个钩子,调用 next 才会执行下一个钩子函数 const iterator = (hook: NavigationGuard, next) => { // 在当前导航还没有完成之前又有了一个新的导航 // 比如,在等待导航守卫的过程中又调用了 router.push // 这时候需要报一个 cancel 的错误 if (this.pending !== route) { return abort(createNavigationCancelledError(current, route)) } // 执行当前的钩子,但用户传入的导航守卫有可能会出错,需要 try 一下 try { // 这就是路由钩子函数的参数:to, from, next hook(route, current, (to: any) => { // 可以通过 next('/login') 这样的方式来重定向 // 如果传入 false 则中断当前的导航,并将 URL 重置到from 路由对应的地址 if (to === false) { this.ensureURL(true) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { // 如果传入 next 的参数是一个 Error 实例, // 则导航会被终止且该错误会被传递给 router.onError() 注册过的回调 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 { // 不符合则跳转至 to next(to) } }) } catch (e) { // 走错误的回调 abort(e) } } // 执行队列 // queue 就是上面 钩子函数 的队列 // iterator 传入 to, from, next,只有执行 next 才进入下一项 // cb 回调函数,当执行完整个队列后调用 // 注意:这里嵌套执行了两次 runQueue, 这是因为前面构造的 queue 只是 vue-router 完整的导航解析流程的 2-6 步,接下来要执行的是 7-9 步 runQueue(queue, iterator, () => { // 这时候异步组件已经解析完成 // 下面是构造 beforeRouteEnter 和 beforeRouteResolve 守卫的队列 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 // 这里是调用 transitionTo 传入的 onComplete 回调 // 在这里会做一些更新路由、URL、调用 afterHooks、onReady 的回调 onComplete(route) if (this.router.app) { // 下次更新 DOM 时触发 handleRouteEntered this.router.app.$nextTick(() => { handleRouteEntered(route) }) } }) })上述代码阐述了
vue-router导航流程2-9步,首先将2-6步的得导航守卫存到queue中,通过runQueue函数,依次执行里面的回调函数。看看runQueue函数具体做了什么:/** * 步骤: * 从 0 开始顺序遍历 queue 中的每一项,在调用 fn 时作为第一个参数传入 * 当使用者调用了第二个参数的回调时, 才进入下一项 * 最后遍历完 queue 中的所有项后,调用 cb 回到参数 * @param {*} queue 钩子函数队列 * @param {*} fn 迭代器函数 * @param {*} cb 迭代器函数执行完后的回调函数 */ 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-6步的导航守卫函数做包装存入到
queue队列中,包装成(from, to, next) => {...}; - 定义迭代器方法;
- 通过
runQueue函数,依次执行迭代器函数,实际上就是遍历queue队列中的方法,依次执行; - 将7-9步的导航存放到
queue队列中,然后同上,执行完导航守卫方法后,执行onComplete方法,即confirmTransition方法传入的第二个参数; - 执行
init中的history.setupListeners函数。
- 先将2-6步的导航守卫函数做包装存入到
3. push和replace方法
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
)
}
replace (location: RawLocation, onComplete?: Function, onAbort?: Function) {
const { current: fromRoute } = this
this.transitionTo(
location,
route => {
replaceHash(route.fullPath)
handleScroll(this.router, route, fromRoute, false)
onComplete && onComplete(route)
},
onAbort
)
}
其实push和replace方法实际上也是调用的transitionTo方法,该方法上述讲过,流程与上述一致。
四、<router-view>和<router-link>
1. <router-view>组件
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
// 缓存路由视图,keepAlive 时会用到
const cache = parent._routerViewCache || (parent._routerViewCache = {})
...
// 这里渲染已经缓存的视图
if (inactive) {
const cachedData = cache[name]
const cachedComponent = cachedData && cachedData.component
if (cachedComponent) {
// #2301
// pass props
if (cachedData.configProps) {
fillPropsinData(cachedComponent, data, cachedData.route, cachedData.configProps)
}
return h(cachedComponent, data, children)
} else {
return h()
}
}
// 拿到对应的视图组件
const matched = route.matched[depth]
const component = matched && matched.components[name]
if (!matched || !component) {
cache[name] = null
return h()
}
// cache component
cache[name] = { component }
...
// 渲染组件
return h(component, data, children)
}
}
可以看到router-view组件时一个函数组件,传入一个属性是name属性,通过$route.components拿到渲染的组件数据,并最后做缓存并渲染。
2. <router-link>组件
export default {
name: 'RouterLink',
props: {
to: {
type: toTypes,
required: true
},
tag: {
type: String,
default: 'a'
},
custom: Boolean,
exact: Boolean,
exactPath: Boolean,
append: Boolean,
replace: Boolean,
activeClass: String,
exactActiveClass: String,
ariaCurrentValue: {
type: String,
default: 'page'
},
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
// Support global empty active class
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 = route.redirectedFrom
? createRoute(null, normalizeLocation(route.redirectedFrom), null, router)
: route
classes[exactActiveClass] = isSameRoute(current, compareTarget, this.exactPath)
classes[activeClass] = this.exact || this.exactPath
? classes[exactActiveClass]
: isIncludedRoute(current, compareTarget)
const ariaCurrentValue = classes[exactActiveClass] ? this.ariaCurrentValue : null
// 事件处理,不一定是 click,取决于用户传入的 event
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
}
const data: any = { class: classes }
const scopedSlot =
!this.$scopedSlots.$hasNormal &&
this.$scopedSlots.default &&
this.$scopedSlots.default({
href,
route,
navigate: handler,
isActive: classes[activeClass],
isExactActive: classes[exactActiveClass]
})
if (scopedSlot) {
...
}
if (this.tag === 'a') {
data.on = on
data.attrs = { href, 'aria-current': ariaCurrentValue }
} else {
...
}
return h(this.tag, data, this.$slots.default)
}
}
该组件也是个函数组件,传递的参数在props里,将数据放在data对象上,包含attrs和on属性,然后通过h函数渲染,即Vue源码里的_createElement方法。
上述阐述只是大概流程,具体细节还需自己debug去看,欢迎指错,共同学习~