前言
最近抽时间学习了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实例中注入router和router和router和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的分析放在下一篇进行分析。