前言 本文是针对 vue-router v3.5.2 版本的一次源码解析,由于水平有限,有些地方写得比较混乱,还望多多包涵。
希望本文能够给那些想阅读 vue-router 源代码却又不知从何上手的同学们给予一些帮助。
一、 new Router 时发生了什么? 对应源码在 src/index.js ,下面讲一下它做了哪些操作:
1. 声明一些变量 1 2 3 4 5 6 7 8 9 10 11 12 this .app = null this .apps = []this .options = optionsthis .beforeHooks = []this .resolveHooks = []this .afterHooks = []this .matcher = createMatcher(options.routes || [], this )
2. 创建 matcher
createMatcher
的源码位置在 src/create-matcher.js ,这个方法的整体是这样的:
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 export function createMatcher ( routes: Array <RouteConfig>, router: VueRouter ): Matcher { const { pathList, pathMap, nameMap } = createRouteMap(routes) function addRoutes (routes ) {} function addRoute (parentOrRoute, route ) {} function getRoutes ( ) {} 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, addRoute, getRoutes, addRoutes } }
这里主要是两点:
根据传入的路由配置生成三张表:pathList
、 pathMap
、 nameMap
,这样后面就可以更高效地访问路由表。
返回一些方法,让它可以获取,操作这三张表。
3. 根据路由配置生成三张表 createRouteMap
的源码位置在 src/create-route-map.js ,可以点开来对照看。
我们先不管其它逻辑,只关注它在第一次时是如何生成这三张表的,其核心逻辑是如下:
1 2 3 routes.forEach((route ) => { addRouteRecord(pathList, pathMap, nameMap, route, parentRoute) })
这里给循环调用了 addRouteRecord
方法,它就在同一个文件中,总结一下它做了如下操作:
首先检查是否有配置 pathToRegexpOptions
参数,这个属性值是路由高级匹配模式中(path-to-regexp
)的参数。
调用 normalizePath
将 path
标准化,比较重要的是这里会将子路由的 path
父路由的 path
拼接在一起。
处理 caseSensitive
参数,它也是 path-to-regexp
中的参数。
声明一个 RouteRecord
,主要代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 : [] }
如果该路由存在子路由,则递归调用 addRouteRecord
添加路由记录。
将这条 RouteRecord
存入 pathList
。
将这条记录以 path
作为 key
存入 pathMap
。
如果存在 alias
,则用 alias
作为 path
再添加一条路由记录。
如果存在 name
,则以 name
作为 key
存入 nameMap
。
到这里,已经搞懂如何生成这三张表了。
4. 使用一些方法来操作这三张表 接着我们回到 createMatcher
方法内部,可以看到它返回的一些方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 export function createMatcher ( routes: Array <RouteConfig>, router: VueRouter ): Matcher { return { match, addRoute, getRoutes, addRoutes } }
其内部无非就是根据这三张表来做一些匹配或者改动而已,后面也基本会提到,这里就先略过。
到这里 createMatcher
的操作就基本讲完了,下面我们回到 new Router
本身。
5. 检查 mode
,使用对应的路由模式 根据传入的路由配置创建一系列的数据表后,下面就要根据不同的 mode
来做不同的操作,核心代码如下:
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 let mode = options.mode || 'hash' this .fallback = mode === 'history' && !supportsPushState && options.fallback !== false if (this .fallback) { mode = 'hash' } if (!inBrowser) { mode = 'abstract' } this .mode = modeswitch (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} ` ) } }
关于三种 mode 有何不同下面会讲。
到这里, new Router()
的整个过程就基本讲完了。
二、 use Router 时发生了什么? 我们知道仅仅通过 new Router()
来构造一个 vue-router 实例后,还需要通过 Vue.use(router)
才能真正在项目中使用它,下面就来讲讲这过程到底发生了什么?
1. Vue.use 源码 在这之前,我们先来看看 Vue.use 做了哪些操作,它的源码在 src/core/global-api/use.js :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import { toArray } from '../util/index' export function initUse (Vue: GlobalAPI ) { Vue.use = function (plugin: Function | Object ) { const installedPlugins = this ._installedPlugins || (this ._installedPlugins = []) if (installedPlugins.indexOf(plugin) > -1 ) { return this } const args = toArray(arguments , 1 ) args.unshift(this ) if (typeof plugin.install === 'function' ) { plugin.install.apply(plugin, args) } else if (typeof plugin === 'function' ) { plugin.apply(null , args) } installedPlugins.push(plugin) return this } }
代码不长,就是接受一个 plugin
,这个 plugin
要么是一个函数,要么就是一个有 install
方法的对象,然后 Vue 会调用这方法,并且将当前 Vue 作为参数传入,以便插件对 Vue 来进行扩展,最后将 plugin
传入 installedPlugins
中,防止重复调用。
2. 安装 Router 然后我们看看在 Vue 安装 VueRouter 时,VueRouter 会做哪些操作,它的源码在 src/install.js :
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 import View from './components/view' import Link from './components/link' export let _Vueexport function install (Vue ) { if (install.installed && _Vue === Vue) return install.installed = true _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) } } 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 ) } }) 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) const strats = Vue.config.optionMergeStrategies strats.beforeRouteEnter = strats.beforeRouteLeave = strats.beforeRouteUpdate = strats.created }
我们看到它调用了 router.init()
这个方法,它的源码在 src/index.js :
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 init (app: any ) { process.env.NODE_ENV !== 'production' && assert( install.installed, `not installed. Make sure to call \`Vue.use(VueRouter)\` ` + `before creating root instance.` ) this .apps.push(app) app.$once('hook:destroyed' , () => { const index = this .apps.indexOf(app) if (index > -1 ) this .apps.splice(index, 1 ) if (this .app === app) this .app = this .apps[0 ] || null if (!this .app) this .history.teardown() }) if (this .app) { return } this .app = app 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 ) } history.listen(route => { this .apps.forEach(app => { app._route = route }) }) }
相信现在对安装 VueRouter 时的大致流程已经很清楚了,我们还看到了它会调用一些很重要的方法,这些方法会从后面的问题中继续深入探讨。
三、 切换路由时发生了什么 下面我们看看 vue-router 在切换路由时做了哪些操作,首先回想一下我们平时使用 vue-router 时有哪些操作可以切换路由?
切换路由的几种方式 我们可以通过以下方式切换不同的路由:
手动触发 URL 更新
点击 router-link
通过 this.$router
的 push
、replace
等方法
1. 手动触发 URL 更新 只要我们更新了 URL
,vue-router 都会相应执行切换路由的逻辑,能更新 URL
操作有以下:
如支持 history
api
history.pushState
history.replaceState
history.back
history.go
location.href = 'xxx'
location.hash = 'xxx'
vue-router 是如何监听这些操作的呢?其实只要监听 popstate
或者 hashchange
就可以了,不过这部分留到后面讲 history
实现时再仔细讲,这里先略过。
2. 通过 router-link
切换 还有就是通过 router-link
组件的方式来切换,这个组件相信大家已经很熟悉了,它的源码在 src/components/link.js ,我们直接看最关键的部分:
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 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 } function guardEvent (e ) { if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return if (e.defaultPrevented) return if (e.button !== undefined && e.button !== 0 ) return if (e.currentTarget && e.currentTarget.getAttribute) { const target = e.currentTarget.getAttribute('target' ) if (/\b_blank\b/i .test(target)) return } if (e.preventDefault) { e.preventDefault() } return true }
很明显, router-link
本质上也是通过 router 的方法来切换路由,那下面就来看看 router 的方法。
3. 通过 router 的方法切换 通过 router 方法来切换路由主要是三个:
push
replace
go
当然还有其它的,比如 resolve
,但这个方法并不是切换路由,但只是把对应路由信息返回过来,这里就不谈了。
其实不同的 mode
它们的实现是不一样的,这里我们就拿最常用的 hash
模式来讲,其它 mode
的方法实现会在后面将不同的 mode
的差异时讨论。
下面是这些方法在 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 28 29 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 ) } go (n: number ) { window .history.go(n) }
可以看到,除了 go
之外(它是通过事件监听器),都是在调用 transitionTo
这个方法,下面我们就看看这个方法的内部。
切换过程 1. 调用 transitionTo 方法 前面我们得知切换路由实际上都在调用 transitionTo
,它是一个 History
基类的方法,三种 mode
都是共用的同一个,下面是它的实现:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 transitionTo (location: RawLocation, onComplete ? : Function , onAbort ? : Function ) { let route 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, () => { this .updateRoute(route) onComplete && onComplete(route) this .ensureURL() this .router.afterHooks.forEach((hook ) => { hook && hook(route, prev) }) if (!this .ready) { this .ready = true this .readyCbs.forEach((cb ) => { cb(route) }) } }, (err ) => { if (onAbort) { onAbort(err) } if (err && !this .ready) { if (!isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START) { this .ready = true this .readyErrorCbs.forEach((cb ) => { cb(err) }) } } } ) }
在上面这段方法中我们得知,要切换路由首先调用 match
方法来匹配到待切换的路由,下面我们看看实现。
2. 调用 match
方法匹配路由 在 transitionTo
中调用的是 router
的 match
方法:
1 2 3 match (raw: RawLocation, current?: Route, redirectedFrom?: Location): Route { return this .matcher.match(raw, current, redirectedFrom) }
而它实际上是调用了 matcher
的 match
方法,这个方法我们之前在 创建 match 这一小节有提到过,下面是它的实现:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 function match ( raw: RawLocation, currentRoute?: Route, redirectedFrom?: Location ): Route { const location = normalizeLocation(raw, currentRoute, false , router) const { name } = location if (name) { 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) 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) } else if (location.path) { 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) }
如果是通过 path
的方式跳转,由于 path
可能会携带一些 params
的信息,前面我们已经提到过初始化路由 信息时,会为每条路由生成一个正则表达式,所以这里就可以根据这个正则来检查是否符合当前路由,也就是上面提到 matchRoute
作用,下面是它的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function matchRoute (regex: RouteRegExp, path: string, params: Object ): boolean { const m = path.match(regex) if (!m) { return false } else if (!params) { return true } for (let i = 1 , len = m.length; i < len; ++i) { const key = regex.keys[i - 1 ] if (key) { params[key.name || 'pathMatch' ] = typeof m[i] === 'string' ? decode(m[i]) : m[i] } } return true }
到这里,关于如何匹配对应的路由已经讲完了,下面我们讲讲匹配到之后它还会做什么?
3. 调用 confirmTransition 方法 前面我们在 1. 调用 transitionTo 方法 时讲到它拿到匹配的路由之后,就会调用 confirmTransition
方法,下面是它的实现:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 confirmTransition (route: Route, onComplete: Function , onAbort?: Function ) { const current = this .current 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)) } const { updated, deactivated, activated } = resolveQueue( this .current.matched, route.matched ) const queue: Array <?NavigationGuard> = [].concat( extractLeaveGuards(deactivated), this .router.beforeHooks, extractUpdateHooks(updated), activated.map((m ) => m.beforeEnter), resolveAsyncComponents(activated) ) const iterator = (hook: NavigationGuard, next ) => { if (this .pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any ) => { if (to === false ) { this .ensureURL(true ) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this .ensureURL(true ) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeofto.name === 'string' )) ) { abort(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this .replace(to) } else { this .push(to) } } else { next(to) } }) } catch (e) { abort(e) } } runQueue(queue, iterator, () => { 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 onComplete(route) if (this .router.app) { this .router.app.$nextTick(() => { handleRouteEntered(route) }) } }) }) }
4. 构造导航守卫队列 我们知道在切换路由时需要执行一系列的导航守卫和路由相关的生命周期,下面就讲讲它的实现,其实也是在 confirmTransition
这个方法中。
第一步就是构造队列,关于它们执行的顺序可以看回文档。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 const queue: Array <?NavigationGuard> = [].concat( extractLeaveGuards(deactivated), this .router.beforeHooks, extractUpdateHooks(updated), activated.map((m ) => m.beforeEnter), resolveAsyncComponents(activated) )
还记得前面我们讲了 updated, deactivated, activated
这三个数组是从 resolveQueue
方法中获取:
1 2 3 4 const { updated, deactivated, activated } = resolveQueue( this .current.matched, route.matched )
下面是它的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 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) } }
实现原理很简单,只需要对比 current
和 next
的 match
数组,就能拿到以下数组:
updated
:有交集的部分
activated
:next 有并且 current 没有的部分
deactivated
:current 有并且 next 没有的部分
下面是队列中各项的实现
调用 extractLeaveGuards(deactivated)
执行销毁的组件 beforeRouteLeave
生命周期:
1 2 3 4 function extractLeaveGuards (deactivated: Array <RouteRecord> ): Array <?Function > { return extractGuards(deactivated, 'beforeRouteLeave' , bindGuard, true ) }
调用全局的 beforeHooks
,其实也就是存放用户通过 beforeEach
注册的数组:
1 2 3 beforeEach (fn: Function ): Function { return registerHook(this .beforeHooks, fn) }
调用 extractUpdateHooks(updated)
执行更新的组件:
1 2 3 function extractUpdateHooks (updated: Array <RouteRecord> ): Array <?Function > { return extractGuards(updated, 'beforeRouteUpdate' , bindGuard) }
调用所有激活组件的 beforeEnter
生命周期:
1 activated.map((m ) => m.beforeEnter)
调用 resolveAsyncComponents(activated)
来解析异步组件:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 export function resolveAsyncComponents (matched: Array <RouteRecord> ): Function { return (to, from , next ) => { let hasAsync = false let pending = 0 let error = null flatMapComponents(matched, (def, _, match, key ) => { if (typeof def === 'function' && def.cid === undefined ) { hasAsync = true pending++ const resolve = once((resolvedDef ) => { if (isESModule(resolvedDef)) { resolvedDef = resolvedDef.default } def.resolved = typeof resolvedDef === 'function' ? resolvedDef : _Vue.extend(resolvedDef) match.components[key] = resolvedDef pending-- if (pending <= 0 ) { next() } }) const reject = once((reason ) => { const msg = `Failed to resolve async component ${key} : ${reason} ` process.env.NODE_ENV !== 'production' && warn(false , msg) if (!error) { error = isError(reason) ? reason : new Error (msg) next(error) } }) let res try { res = def(resolve, reject) } catch (e) { reject(e) } if (res) { if (typeof res.then === 'function' ) { res.then(resolve, reject) } else { const comp = res.component if (comp && typeof comp.then === 'function' ) { comp.then(resolve, reject) } } } } }) if (!hasAsync) next() } }
异步加载这一块其实涉及比较多,深入讲的话还要讲 webpack
,所以这里只讲大概的流程,以后有机会的话再深入讲解。
可以看到执行导航守卫都是通过调用一个 extractGuards
方法,下面是它的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 function extractGuards ( records: Array <RouteRecord>, name: string, bind: Function , reverse?: boolean ): Array <?Function > { const guards = flatMapComponents(records, (def, instance, match, key ) => { const guard = extractGuard(def, name) if (guard) { return Array .isArray(guard) ? guard.map((guard ) => bind(guard, instance, match, key)) : bind(guard, instance, match, key) } }) return flatten(reverse ? guards.reverse() : guards) }
在仔细讲这个方法内部逻辑前,要先搞清楚这三个方法的内部: extractGuard
、 bindGuard
、 flatMapComponents
:
extractGuard
很简单,其实就是获取 vue 组件实例中特定的生命周期:
1 2 3 4 5 6 7 8 9 10 function extractGuard ( def: Object | Function , key: string ): NavigationGuard | Array <NavigationGuard > { if (typeof def !== 'function' ) { def = _Vue.extend(def) } return def.options[key] }
bindGuard
的作用就是返回一个函数,这个函数会调用组件特定生命周期,给后续执行队列时调用:
1 2 3 4 5 6 7 8 9 10 11 function bindGuard (guard: NavigationGuard, instance: ?_Vue ): ?NavigationGuard { if (instance) { return function boundRouteGuard ( ) { return guard.apply(instance, arguments ) } } }
而 flatMapComponents
顾名思义就是跟组件相关的,它的作用是依次遍历传入的 matched
数组相关的组件,并调用传入的回调的返回值作为自己的返回值,所以它的返回值是调用者决定的:
1 2 3 4 5 6 7 8 9 function flatMapComponents (matched, fn ) { return flatten( matched.map(function (m ) { return Object .keys(m.components).map(function (key ) { return fn(m.components[key], m.instances[key], m, key) }) }) ) }
所以现在再回过头来看 extractGuards
就很清晰了,它的作用就是通过 flatMapComponents
遍历所有 match
数组中的组件,并通过 extractGuard
拿到这些组件的特定生命周期,然后通过 bindGuard
返回一个可以调用这个生命周期的函数,然后利用 flatten
将它们扁平化,根据 reverse
决定是否倒序返回这些函数数组。
最后这些函数全部放在 queue
中,这就是构造整个队列的过程了。
5. 执行队列 构造完队列,下面就要开始执行这个队列了,在这之前我们先来看看 runQueue
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 ) }
其实也不复杂,首先从 0 开始按顺序遍历 queue
中的每一项,在调用 fn
时作为第一个参数传入,当使用者调用了第二个参数的回调时,才进入下一次项,最后遍历完 queue
中所有的项后,调用 cb
回到参数。
下面是执行这个队列的过程:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 runQueue(queue, iterator, () => { 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 onComplete(route) if (this .router.app) { this .router.app.$nextTick(() => { handleRouteEntered(route) }) } }) })
iterator
的定义在 1. 调用 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 36 37 38 39 40 41 42 43 44 45 46 47 48 const iterator = (hook: NavigationGuard, next ) => { if (this .pending !== route) { return abort(createNavigationCancelledError(current, route)) } try { hook(route, current, (to: any ) => { if (to === false ) { this .ensureURL(true ) abort(createNavigationAbortedError(current, route)) } else if (isError(to)) { this .ensureURL(true ) abort(to) } else if ( typeof to === 'string' || (typeof to === 'object' && (typeof to.path === 'string' || typeofto.name === 'string' )) ) { abort(createNavigationRedirectedError(current, route)) if (typeof to === 'object' && to.replace) { this .replace(to) } else { this .push(to) } } else { next(to) } }) } catch (e) { abort(e) } }
但是我们留意到这里其实是嵌套执行了两次 runQueue
,这是因为我们前面构造的 queue
只是 vue-router 完整的导航解析流程中的 第 26 步,而接下来就要执行第 79 步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 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 onComplete(route) if (this .router.app) { this .router.app.$nextTick(() => { handleRouteEntered(route) }) } })
6. 执行 confirmTransition
后的操作 到这里 confirmTransition
方法就已经执行完了,最后会调用 transitionTo
传入的 onComplete
方法,之前就有提到:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 this .updateRoute(route)onComplete && onComplete(route) this .ensureURL()this .router.afterHooks.forEach((hook ) => { hook && hook(route, prev) }) if (!this .ready) { this .ready = true this .readyCbs.forEach((cb ) => { cb(route) }) }
这里主要做了几步:更新当前路由、调用传入的 onComplete
、更新 URL
、调用 afterHooks
、 onReady
钩子。
而如果 confirmTransition
执行失败的话,则会执行传入的 onAbort
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 if (onAbort) { onAbort(err) } if (err && !this .ready) { if ( !isNavigationFailure(err, NavigationFailureType.redirected) || prev !== START ) { this .ready = true this .readyErrorCbs.forEach((cb ) => { cb(err) }) } }
主要是调用传入的 onAbort
回调,执行 onError
钩子。
到这里整个 transitionTo
方法的执行过程已经讲完了,导航守卫和一些钩子函数也已经全部执行完毕。
7. 更新路由信息 接着我们看看它是如何更新当前路由信息的,也就是 updateRoute
方法:
1 2 3 4 updateRoute (route: Route ) { this .current = route this .cb && this .cb(route) }
首先是更新一下 current
的指向,接着调用 cb
这个回调函数并且将当前路由传入,那这个 cb
是什么东西呢?它是在 listen
方法中被赋值的:
1 2 3 listen (cb: Function ) { this .cb = cb }
而哪里调用了这个 listen
方法呢?我们看回之前在 2. 安装 Router 时初始化那里的一段代码:
1 2 3 4 5 6 7 history.listen((route ) => { this .apps.forEach((app ) => { app._route = route }) })
所以到这里,我们通过 this.$route
拿到的路由就已经变成跳转的路由了。
8. 更新 URL 接着就是更新 URL
了,在 transitionTo
这里它是先调用了 onComplete
方法,然后再调用 ensureURL
方法来更新浏览器上的 URL
,对应源码:
1 2 3 4 onComplete && onComplete(route) this .ensureURL()
由于我们这里是以 hash
模式来展开的,所以我们看看它的 push
方法里传入的 onComplete
方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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 ) }
而 pushHash
这里实际到后面已经可以更新 URL
:
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 export function pushState (url?: string, replace?: boolean ) { saveScrollPosition() 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) } } function pushHash (path ) { if (supportsPushState) { pushState(getUrl(path)) } else { window .location.hash = path } }
所以后面再执行 ensureURL
时就不需要再更新一遍了:
1 2 3 4 5 6 ensureURL (push ? : boolean ) { const current = this .current.fullPath if (getHash() !== current) { push ? pushHash(current) : replaceHash(current) } }
难道这个 ensureURL
就是多此一举吗?也不是,在其他地方调用就会更新 URL
的,比如 transitionTo
检查是否跳转至相同路径:
1 2 3 4 5 6 7 8 9 if ( isSameRoute(route, current) && lastRouteIndex === lastCurrentIndex && route.matched[lastRouteIndex] === current.matched[lastCurrentIndex] ) { this .ensureURL() return abort(createNavigationDuplicatedError(current, route)) }
这里更新了 URL
以后,还会调用 handleScroll
来滚动相关的操作,如:保存当前滚动位置、根据传入的 scrollBehavior
设置当前滚动位置,不过这里就不展开讲了。
另外,更新 URL
这部分行为也是根据不同的路由模式有所区别,后面的章节会详情讲解。
9. 渲染对应的路由视图 除了更新 URL
以外,我们还要渲染当前路由对应的视图,那这又是如何做到的呢?我们知道 vue-router 是通过一个叫 router-view
的组件来渲染,下面看看它的实现,它的源码在:src/components/view.js ,我们先粗略看一下它的 render
方法:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 render (_, { props, children, parent, data } ) { data.routerView = true const h = parent.$createElement const name = props.name const route = parent.$route const cache = parent._routerViewCache || (parent._routerViewCache = {}) let depth = 0 let inactive = false while (parent && parent._routerRoot !== parent) { const vnodeData = parent.$vnode ? parent.$vnode.data : {} if (vnodeData.routerView) { depth++ } if (vnodeData.keepAlive && parent._directInactive && parent._inactive) { inactive = true } parent = parent.$parent } data.routerViewDepth = depth if (inactive) { const cachedData = cache[name] const cachedComponent = cachedData && cachedData.component if (cachedComponent) { 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[name] = { component } data.registerRouteInstance = (vm, val ) => { const current = matched.instances[name] if ((val && current !== vm) || (!val && current === vm)) { matched.instances[name] = val } } ; (data.hook || (data.hook = {})).prepatch = (_, vnode ) => { matched.instances[name] = vnode.componentInstance } data.hook.init = (vnode ) => { if ( vnode.data.keepAlive && vnode.componentInstance && vnode.componentInstance !== matched.instances[name] ) { matched.instances[name] = vnode.componentInstance } handleRouteEntered(route) } const configProps = matched.props && matched.props[name] if (configProps) { extend(cache[name], { route, configProps }) fillPropsinData(component, data, route, configProps) } return h(component, data, children) }
可以看到 router-view
是通过 $route
变量来获取当前组件的,而在前面 7. 更新路由信息 时有提到会更新 _route
变量,而它在 2. 安装 Router 时就已经用 $route
包装成响应式了,这里自然也就可以渲染对应的组件了。
四、 动态添加路由实现 我们在开发时可能会遇到一些比较复杂的场景,需要动态添加路由,最常见的例子就是根据后端返回的不同用户角色去配置不同的前端路由,那下面就讲讲它在 vue-router 内部是如何实现的。
我们只需要使用 router.addRoute
方法就能新增一条路由记录,之前我们在讲 2. 创建 matcher 有看到这个方法的定义,下面是它的实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 function addRoute (parentOrRoute, route ) { const parent = typeof parentOrRoute !== 'object' ? nameMap[parentOrRoute] : undefined createRouteMap([route || parentOrRoute], pathList, pathMap, nameMap, parent) if (parent) { createRouteMap( parent.alias.map((alias ) => ({ path : alias, children : [route] })), pathList, pathMap, nameMap, parent ) } }
这里比较重要的是调用 createRouteMap
来创建路由,它的实现之前在 3. 根据路由配置生成三张表 有提到,不过当时只关注它如何生成三张表,在现在这种情况下调用它的区别在于:
1 2 3 4 const pathList: Array <string> = oldPathList || []const pathMap: Dictionary<RouteRecord> = oldPathMap || Object .create(null )const nameMap: Dictionary<RouteRecord> = oldNameMap || Object .create(null )
好了,可以看到新增一条路由规则十分简单,只需要对 pathList
、 pathMap
、 nameMap
进行改动就好了。
五、 三种路由模式的实现 vue-router 的核心逻辑已经讲得差不多了,就剩下三种路由模式之间的差异,这一小节就来仔细讲讲它们各自的内部实现。
相同的部分 我们知道三种路由模式都是 History
的派生类,源码位置在 src/history/base.js ,我们先来看看它们一些比较重要的公用方法:
onReady
onError
transitionTo
confirmTransition
updateRoute
其实这些方法在前文中已经或多或少有提到了,其余的那些也只是做一些更新变量的操作,这里也不谈了。
其实还有一个非常重要的就是构造函数,它主要是做一些实例变量的初始化,这里混个眼熟就好:
1 2 3 4 5 6 7 8 9 10 11 12 constructor (router: Router, base: ? string ) { this .router = router this .base = normalizeBase(base) this .current = START this .pending = null this .ready = false this .readyCbs = [] this .readyErrorCbs = [] this .errorCbs = [] this .listeners = [] }
下面我们就讲讲它们不同的地方。
hash 模式 hash 应该是最常用的一种模式了,它也是浏览器环境下的默认模式,至于它的特点相信大家也很熟悉了,就是利用 URL
中的 hash
值来做路由,这种模式兼容性是最好的。
初始化 我们先来看看它在初始化时会做哪些操作:
1 2 3 4 5 6 7 8 constructor (router: Router, base: ? string, fallback : boolean ) { super (router, base) if (fallback && checkFallback(this .base)) { return } ensureSlash() }
代码不多,首先是检查是否因为回退而使用 hash 模式,如果是的话则调用 checkFallback
检查它的返回值,如果为 true
则不调用 ensureSlash
。
下面是 checkFallback
的实现:
1 2 3 4 5 6 7 8 9 10 11 12 function checkFallback (base ) { const location = getLocation(base) if (!/^\/#/ .test(location)) { window .location.replace(cleanPath(base + '/#' + location)) return true } }
也就是说当我们使用了 history
模式但由于不支持需要回退到 hash
模式时,它会自动重定向到符合 hash
模式下的 url
,接着再执行 ensureSlash
方法。
下面是 ensureSlash
方法的实现:
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 function getHash ( ): string { let href = window .location.href const index = href.indexOf('#' ) if (index < 0 ) return '' href = href.slice(index + 1 ) return href } function replaceHash (path ) { if (supportsPushState) { replaceState(getUrl(path)) } else { window .location.replace(getUrl(path)) } } function ensureSlash ( ): boolean { const path = getHash() if (path.charAt(0 ) === '/' ) { return true } replaceHash('/' + path) return false }
很简单,就是判断一下 hash
部分是否以 /
开头,如果不是则要重定向到以 /
开头的 URL
。
这样就能解释我们在使用 vue-router 开发项目时,为什么打开调试页面 http://localhost:8080 后会自动把 url 修改为 http://localhost:8080/#/ 了。
push 和 replace hash
模式的 push
方法我们在 三、 切换路由时发生了什么 这一小节已经提到过了,其实 replace
也是大同小异,下面是这两个方法的实现:
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 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 ) }
replace
方法跟 push
方法不同的地方是它调用的是 replaceHash
而不是 pushHash
,下面是 replaceHash
方法的实现:
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 function replaceHash (path ) { if (supportsPushState) { replaceState(getUrl(path)) } else { window .location.replace(getUrl(path)) } } export function pushState (url?: string, replace?: boolean ) { saveScrollPosition() 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
时的区别在于调用的是 push
还是 replace
方法。
go 而 go
方法就更直接了,实际上就是调用 history.go
这个方法:
1 2 3 go (n: number ) { window .history.go(n) }
不知道大家会不会疑惑,这里没有调用 transitionTo
方法, vue-router
是如何知道需要更新路由的呢?
这就是就得不得说一下 setupListeners
这个方法了。
setupListeners 还记得在 三、 切换路由时发生了什么 这一小节的 init
方法里有这么一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 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 ) }
在 history
或者 hash
模式下初始化时也会调用一下 transitionTo
,而这里传入的 onComplete
回调就会调用 setupListeners
方法,为什么要这么做呢?我们直接看 setupListeners
里面是什么:
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 setupListeners ( ) { if (this .listeners.length > 0 ) { return } const router = this .router const expectScroll = router.options.scrollBehavior const supportsScroll = supportsPushState && expectScroll if (supportsScroll) { this .listeners.push(setupScroll()) } const handleRoutingEvent = () => { const current = this .current if (!ensureSlash()) { return } this .transitionTo(getHash(), (route ) => { if (supportsScroll) { handleScroll(this .router, route, current, true ) } if (!supportsPushState) { replaceHash(route.fullPath) } }) } const eventType = supportsPushState ? 'popstate' : 'hashchange' window .addEventListener(eventType, handleRoutingEvent) this .listeners.push(() => { window .removeEventListener(eventType, handleRoutingEvent) }) }
主要是做了两件事情:
监听 popstate
或者 hashchange
事件,触发时会执行一下 transitionTo
在 listeners
中存入两个回调:处理滚动相关、取消监听第 1 点中的事件
也就是说 vue-router 除了调用 push
或者 replece
这些方法以外,它也支持通过其它方式来切换路由,只要这个操作会触发 popstate
或者 hashchange
事件,比如下面这些方式:
如支持 history
api
history.pushState
history.replaceState
history.back
history.go
location.hash = '#/a'
当然这个事件监听器会在应用实例销毁时取消监听,避免产生副作用:
1 2 3 4 5 6 7 8 9 10 11 12 teardown ( ) { this .listeners.forEach(cleanupListener => { cleanupListener() }) this .listeners = [] this .current = START this .pending = null }
history 模式 history
模式是基于 HTML5 History API 实现的,不过在生产环境上使用它还需要在服务器上配置路由转发才行,不过这仍是大部分项目的选择,毕竟这样比较好看,不像 hash 模式这么奇葩。
初始化 我们看看 history
模式在初始化时会做哪些操作:
1 2 3 4 5 constructor (router: Router, base: ? string ) { super (router, base) this ._startLocation = getLocation(this .base) }
只是初始化了一个 _startLocation
变量,这个变量的作用后面会讲到。
push 和 replace、go 其实 history
模式的这几个方法与 hash
模式是一模一样的,区别是它们在调用 pushState
时传入的 URL
不一样而已,关于 pushState
方法的定义前面已经讲过了。
setupListeners setupListeners
与 hash
模式也是大同小异,区别在于它在判断 URL
与当前路由是否一致时有点不同:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const handleRoutingEvent = () => { const current = this.current - if (!ensureSlash()) { - return - } + const location = getLocation(this.base) + if (this.current === START && location === this._startLocation) { + return + } this.transitionTo(location, (route) => { if (supportsScroll) { handleScroll(router, route, current, true) } }) }
abstract 模式 abstract
我们可能用得比较少,它主要是用在 node
环境下,也就是说在该模式下不会调用一切与浏览器相关的 api
,那它就只能用别的地方去维护当前 URL
与路由历史,由于不是很长,我直接放在一起讲了:
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 export class AbstractHistory extends History { index : number stack : Array <Route> constructor (router: Router, base: ?string ) { super (router, base) this .stack = [] this .index = -1 } push (location: RawLocation, onComplete?: Function , onAbort?: Function ) { this .transitionTo( location, (route ) => { this .stack = this .stack.slice(0 , this .index + 1 ).concat(route) this .index++ onComplete && onComplete(route) }, onAbort ) } replace (location: RawLocation, onComplete?: Function , onAbort?: Function ) { this .transitionTo( location, (route ) => { this .stack = this .stack.slice(0 , this .index).concat(route) onComplete && onComplete(route) }, onAbort ) } go (n: number ) { const targetIndex = this .index + n if (targetIndex < 0 || targetIndex >= this .stack.length) { return } const route = this .stack[targetIndex] this .confirmTransition( route, () => { const prev = this .current this .index = targetIndex this .updateRoute(route) this .router.afterHooks.forEach((hook ) => { hook && hook(route, prev) }) }, (err ) => { if (isNavigationFailure(err, NavigationFailureType.duplicated)) { this .index = targetIndex } } ) } getCurrentLocation ( ) { const current = this .stack[this .stack.length - 1 ] return current ? current.fullPath : '/' } ensureURL ( ) { } }
本文完,感谢阅读。
🥳 加载 Disqus 评论