之前的博客 我们讲了我们创建VueRouter实例并通过Vue.use应用之后,VueRouter内部如何生成一整套自己的数据结构来存储路由配置的,这次博客我们继续解读Vue Router的源码,大致内容是Vue Router的路由守卫的执行逻辑。
路由守卫这个是VueRouter的路由守卫的官方地址:https://router.vuejs.org/zh/guide/advanced/navigation-guards.html
整个路由变化过程中路由守卫的执行的顺序如下:
导航被触发。 在失活的组件里调用 beforeRouteLeave
守卫。 调用全局的 beforeEach
守卫。 在重用的组件里调用 beforeRouteUpdate
守卫(2.2+)。 在路由配置里调用 beforeEnter。
解析异步路由组件。 在被激活的组件里调用 beforeRouteEnter。
调用全局的 beforeResolve
守卫(2.5+)。 导航被确认。 调用全局的 afterEach
钩子。 触发 DOM 更新。 调用 beforeRouteEnter
守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。 用例还是先讲一下我们本次博客基于的代码
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 import Vue from 'vue' import VueRouter from 'vue-router' Vue .use (VueRouter )const Foo = { template : '<div>foo</div>' , beforeRouteEnter (to, from ) { }, beforeRouteUpdate (to, from ) { }, beforeRouteLeave (to, from ) { } } const Bar = { template : '<div>bar</div>' }const Baz = { template : '<div>baz</div>' }const router = new VueRouter ({ mode : 'history' , base : __dirname, routes : [ { path : '/' , components : { default : Foo , a : Bar , b : Baz beforeEnter : (to, from ) => { console .log (bar); return true }, } }, { path : '/other' , components : { default : Baz , a : Bar , b : Foo } } ] }) router.beforeEach (async (to, from ) => { const canAccess = await canUserAccess (to) if (!canAccess) return '/login' }) router.beforeResolve (async to => { if (to.meta .requiresCamera ) { try { await askForCameraPermission () } catch (error) { if (error instanceof NotAllowedError ) { return false } else { throw error } } } }) router.afterEach ((to, from ) => { sendToAnalytics (to.fullPath ) }) new Vue ({ router, template : ` <div id="app"> <h1>Named Views</h1> <ul> <li><router-link to="/">/</router-link></li> <li><router-link to="/other">/other</router-link></li> </ul> <router-view class="view one"></router-view> <router-view class="view two" name="a"></router-view> <router-view class="view three" name="b"></router-view> </div> ` }).$mount('#app' )
源码分析 push我们从VueRouter.prototype.push开始分析调用路由切换之后发生了什么
1 2 3 4 5 6 7 8 9 10 push (location : RawLocation , onComplete?: Function , onAbort?: Function ) { if (!onComplete && !onAbort && typeof Promise !== 'undefined' ) { return new Promise ((resolve, reject ) => { this .history .push (location, resolve, reject) }) } else { this .history .push (location, onComplete, onAbort) } }
三个参数分别为RawLocation,成功时的回调函数以及失败时的回调函数,RawLocation其实就是我们在正式使用时push函数传入的参数类型,具体定义如下:
1 2 3 4 5 6 7 8 9 10 11 12 declare type Location = { _normalized?: boolean ; name?: string ; path?: string ; hash?: string ; query?: Dictionary <string >; params?: Dictionary <string >; append?: boolean ; replace?: boolean ; } declare type RawLocation = string | Location
之前的博客 我们this.history会根据mode不同实例化为不同的对象,我们以HTML5History来分析
1 2 3 4 5 6 7 8 push (location : RawLocation , onComplete?: Function , onAbort?: Function ) { const { current : fromRoute } = this this .transitionTo (location, route => { pushState (cleanPath (this .base + route.fullPath )) handleScroll (this .router , route, fromRoute, false ) onComplete && onComplete (route) }, onAbort) }
首先是获取当前的路由:current,它的类型是Route
然后调用了transitionTo
函数,传入了两个参数,第一个是location,第二个是回调函数,我们看一下这个transitionTo
做了什么
transitionTo在看transtitionTo
函数之前,我们看一下几个工具函数
runQueue首先是runQueue
:
这个函数有三个参数,分别是:
queue:一个参数的数组 fn:实际执行函数,其参数也有两个,第一个是queue中的每一项,第二个是fn执行完成后的回调函数 cb:针对queue中的每一项都执行过fn后的最终的回调函数 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 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 ) }
简单来说,这个函数的的作用就是从传入的一系列RouteRecord
中,依次抽取出组件内部的路由守卫,然后将这些路由守卫的上下文,也就是this指针通过apply函数指定为自身,如果最后一个参数为true,那就反转这些路由守卫的顺序。
直接对外暴露的函数是,如extractLeaveGuards,它实际上就是调用extractGuards(deactivated, 'beforeRouteLeave', bindGuard, true)
,也就是从反激活的一系列组件中,获取beforeRouteLeave
函数,拼装起来后再反转。
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 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) } function extractGuard ( def : Object | Function , key : string ): NavigationGuard | Array <NavigationGuard > { if (typeof def !== 'function' ) { def = _Vue.extend (def) } return def.options [key] } function extractLeaveGuards (deactivated : Array <RouteRecord > ): Array <?Function > { return extractGuards (deactivated, 'beforeRouteLeave' , bindGuard, true ) } function extractUpdateHooks (updated : Array <RouteRecord > ): Array <?Function > { return extractGuards (updated, 'beforeRouteUpdate' , bindGuard) } function bindGuard (guard : NavigationGuard , instance : ?_Vue ): ?NavigationGuard { if (instance) { return function boundRouteGuard ( ) { return guard.apply (instance, arguments ) } } }
confirmTransition(第1-8步)然后是,confirmTransition
:
这个函数大致分为三部分:
判断是否需要abort,如果需要则退出 生成要执行的路由守卫的数组 依次执行路由守卫的数组 首先是判断一下传入的route和当前的route是否是同一个,如果是的话,直接调用abort并退出
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 const current = this .current this .pending = routeconst abort = err => { if (!isNavigationFailure (err) && isError (err)) { if (this .errorCbs .length ) { this .errorCbs .forEach (cb => { cb (err) }) } else { if (process.env .NODE_ENV !== 'production' ) { 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 () if (route.hash ) { handleScroll (this .router , current, route, false ) } return abort (createNavigationDuplicatedError (current, route)) }
然后是一段生成路由守卫执行顺序的函数逻辑:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 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) )
第一行的resolveQueue
的作用是比较当前的路由和即将跳转的路由之间的变化,它的两个参数的类型都是Array<RouteRecord>
,它的逻辑也比较简单,就是比较两个数组之间的差异:
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) } }
而queue数字中的路由守卫依次是:
所有 deactivated 组件中的 beforeRouteLeave 函数,然后反转顺序 全局的 beforeHooks 函数 所有 updated 组件中的 beforeRouteUpdate 函数 所有 activated 组件对应的路由配置中的 beforeEnter 函数 加载所有 activated 中异步组件的函数 这五步也对应着一开始我们说的路由守卫执行顺序中的2-6步
得到了正确的路由守卫执行顺序的数组后,就是正式执行了,这里就用到了我们刚才说的工具函数: runQueue 了,代码如下:
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 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' || typeof to.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) }) } }) })
首先iterator
函数,两个参数为
然后我们的runQueue,嵌套了两层:
第一层首先是传入了刚才我们生成的queue,也就是2-6步的路由守卫函数,fn就是我们的iterator函数,结合我们一开始解读的runQueue的源码,总起来第一层做的事情就是,依次执行我们2-6步的路由守卫,每次执行完成一个通过调用hook中的next来调用runQueue中的step来执行下一个hook 第一层把所有queue执行完成之后的回调函数中,我们又生成了一层runQueue函数,区别就在于这一次,我们传入的queue不同了,这一次我们的queue生成方法为:const enterGuards = extractEnterGuards(activated); const queue = enterGuards.concat(this.router.resolveHooks)
,两部分组成,第一部分是所有activated组件的 beforeRouteEnter 函数,然后是 this.router.resolveHooks,也就是全局的 beforeResolve ,这里就是路由守卫顺序中的第7,8步 在第二层的runQueue的回调函数中,我们就可以调用confirmTransition的的onComplete函数了 然后我们想要了解这个onComplete做了什么,就要看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 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) }) } } } ) }
comfirmTransition成功回调(第9-12步)可以看到,我们的onComplete其实就是这一段:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 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) }) }
第一步,调用updateRoute函数,这个函数内部其实就是history.listen函数传入的回调函数,这个比较重要,这个回调函数会修改$route的值,而这个值是响应式的,在route-view组件中会使用到,也就是说改变这个值会导致route-view重新调用render函数,也就出发了视图的更新 第二步如果transitionTo有onComplete函数那么调用 依次调用 afterHooks 函数,这个其实就是全局注册的 afterEach 的数组,至此,第11步也完成了 那么最后一步,重新执行beforeRouterEnter,并通过next返回实例是怎么做到的呢?这一点其实和第七步生成的beforeRouterEnter的回调方式有关,这个extractEnterGuards
和其另两个 extractLeaveGuards
,和 extractUpdateHooks
不一样,它的bind函数不是简单的apply,而是调用成功之后再执行next回调函数,具体如下:
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 function extractEnterGuards ( activated : Array <RouteRecord > ): Array <?Function > { return extractGuards ( activated, 'beforeRouteEnter' , (guard, _, match, key ) => { return bindEnterGuard (guard, match, key) } ) } function bindEnterGuard ( guard : NavigationGuard , match : RouteRecord , key : string ): NavigationGuard { return function routeEnterGuard (to, from , next ) { return guard (to, from , cb => { if (typeof cb === 'function' ) { if (!match.enteredCbs [key]) { match.enteredCbs [key] = [] } match.enteredCbs [key].push (cb) } next (cb) }) } }
至此,完成了修改路由之后的1-12的函数执行,并且成功通过响应式的的方法触发了router-view的改变。