「这是我参与2022首次更文挑战的第15天,活动详情查看:2022首次更文挑战」
前言
最近抽时间学习了vueRouter源码,基本也就是走马观花式地看了一遍。虽然很多细节和原理没有去深入分析,但还是想通过博客记录下自己从中学习到的点滴。
目录结构
相对于vue源码来说,router源码的目录结构简单许多
src目录
│ create-matcher.js // matcher相关,主要用于pathList的生成及匹配
│ create-route-map.js // route相关,将path创建为route对象
│ index.js // vueRouter实例相关
│ install.js // 插件安装
│
├─components
│ link.js // router-link组件定义
│ view.js // router-view组件定义
│
├─history // history用于跳转相关处理
│ abstract.js
│ base.js // history基本类其它三个继承自base,分别对应三种不同的路由模式
│ hash.js
│ html5.js
│
└─util // 辅助函数
async.js
dom.js
errors.js
location.js
misc.js
params.js
path.js
push-state.js
query.js
resolve-components.js
route.js
scroll.js
state-key.js
warn.js
插件安装
我们知道vue插件是通过执行install传入Vue来执行install方法进行初始化逻辑的,vueRouter也不例外。我们看看其中几个重要的逻辑。
// install
Object.defineProperty(Vue.prototype, '$router', {
get () { return this._routerRoot._router }
})
Object.defineProperty(Vue.prototype, '$route', {
get () { return this._routerRoot._route }
})
Vue.component('RouterView', View)
Vue.component('RouterLink', Link)
在此通过添加原型方法的方式往vue实例中注入route对象,这就解释了为什么每个组件实例都可以拿到这两个对象或方法。
同时通过Vue.component注册了全局组件RouterView及RouterLink
// install
Vue.mixin({
beforeCreate () {
if (isDef(this.$options.router)) {
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)
}
})
在install中还有个比较重要的就是通过Vue.mixin为实例混入执行逻辑,这边有几个关键的点
- 将_routerRoot设置为根实例,并且子组件通过$parent来获取
if (isDef(this.$options.router)) {
this._routerRoot = this
} else {
this._routerRoot = (this.$parent && this.$parent._routerRoot) || this
}
- 从根实例配置获取router,并通过init方法进行初始化。联系上一步的行为可以得知这边就是将组件实例this和router实例绑定了。所以每个组件都可以通过this._routerRoot获取根实例,并且获取其中的router实例。
// $options.router即我们在new Vue中传入的router
this._router = this.$options.router
// 通过init进行初始化 这边还传入了组件实例this
this._router.init(this)
- 通过vue的defineReactive函数实现数据监听。这里巧妙地使用defineReactive将_routerRoot的_route属性设置为响应式数据,实际在routerview组件中会访问_route属性。所以在我们修改当前路由_route时候就会触发render函数的重新执行,其实也就重新渲染routerview。
Vue.util.defineReactive(this, '_route', this._router.history.current)
VueRouter实现
接着我们来看看VueRouter的重要逻辑,在indexJS中。
在构造函数中会初始化一些重要变量。
// VueRouter constructor
// 保存实例对应的根组件
this.app = null
this.apps = []
// router实例化传入的配置
this.options = options
// 用于收集全局守卫函数的数组
this.beforeHooks = []
this.resolveHooks = []
this.afterHooks = []
// createMatcher将options.routes作为参数传入
// 将路由配置routes进一步处理生成pathList pathMap nameMap
// 我们在后面将分析createMatcher
this.matcher = createMatcher(options.routes || [], this)
再来看看对于路由模式的处理,比较简单
// VueRouter constructor
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
// 对于不同路由模式将实例化不同的路由类,如history模式对应HTML5History
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}`)
}
}
我们再简单看下几个主要的实例方法
- init函数将router实例和根组件实例app进行绑定
init(app) {
this.apps.push(app)
if (this.app) {
return
}
this.app = app
const history = this.history
// 比较重要的一步
// 路由的修改处理逻辑实际是通过history实例来处理的
// 这边通过history暴露出的listen方法对路由变化进行监听
// 当路由修改时将调用回调函数更新app._route
// 这就和上面插件安装步骤中将_router定义为响应式数据联系起来了
// 当history改变->app._route修改->routerview重新渲染->更新当前显示的组件
history.listen(route => {
this.apps.forEach(app => {
app._route = route
})
})
}
- match函数进行路由匹配,实际vuerouter将routes的处理和匹配都交给了matcher进行处理。可以理解为将这一部分内容抽离了。
match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route {
return this.matcher.match(raw, current, redirectedFrom)
}
- push和replace等方法。我们知道可以通过router实例调用push方法来切换路由,看看其在router实例中的实现逻辑。
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 {
// 实际router这里并不会直接处理路由修改的逻辑
// 而是将其转交给history进行处理
this.history.push(location, onComplete, onAbort)
}
}
Matcher相关逻辑
前面有提到,Matcher用于处理routes及进行路由匹配的逻辑,实际主要逻辑是生成record及pathList等,我们来看看其相关的几个重要逻辑。
createRouteMap
- 通过createRouteMap将配置routes转化为数组及对象数组
// createMatcher
const { pathList, pathMap, nameMap } = createRouteMap(routes)
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>
} {
// 这边可以留意oldPathList oldPathMap oldPathMap这里其实利用了引用数据来递归生成pathList等
const pathList: Array<string> = oldPathList || []
const pathMap: Dictionary<RouteRecord> = oldPathMap || Object.create(null)
const nameMap: Dictionary<RouteRecord> = || Object.create(null)
// 实际会进一步执行addRouteRecord来往pathList等添加数据
routes.forEach(route => {
addRouteRecord(pathList, pathMap, nameMap, route, parentRoute)
})
// *号会最后匹配的原理就是这里单独拎出来处理了
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
再来看看addRouteRecord的逻辑
首先定义RouteRecord
// RouteRecord实际对应我们在开发中看到的route对象
// addRouteRecord的核心逻辑之一就在于将routes转化为addRouteRecord
const record: RouteRecord = {
path: normalizedPath,
regex: compileRouteRegex(normalizedPath, pathToRegexpOptions),
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 }
}
再就是将routeRecord加到数组中进行保存了
if (!pathMap[record.path]) {
pathList.push(record.path)
pathMap[record.path] = record
}
if (!nameMap[name]) {
nameMap[name] = record
}
还有个逻辑就是递归了,前面只能说对routes最外层进行处理生成record,但是嵌套路由没有处理,实际是通过深度优先遍历来实现嵌套路由处理的。
route.children.forEach(child => {
const childMatchAs = matchAs
? cleanPath(`${matchAs}/${child.path}`)
: undefined
addRouteRecord(pathList, pathMap, nameMap, child, record, childMatchAs)
})
addRoutes
我们知道vueRouter可以通过addRoutes来实现动态路由,实际其原理很简单。
function addRoutes (routes) {
createRouteMap(routes, pathList, pathMap, nameMap)
}
因为pathList和pathMap,nameMap是引用类型,实际再调用createRouteMap生成对应record并加入到其中就可以了。这也解释了一个东西,我们可以添加新的路由,但是不能去更新已有的路由。
match
match就很简单了,通过前面的pathList,pathMap,nameMap查找到对应的record就行,这边不另外分析。
结语
本篇文章简单记录了下vueRouter中几个主要文件的关键逻辑。为避免篇幅过长看着太过枯燥将history及component的分析放在下一篇进行分析。