在本期文章中,我將爲你們梳理弄明白如下幾個事情,vue
在以前說過的一個內容router實例的history屬性幫助咱們作了全部跳轉部分的事情,因此導航守衛的內容也在history中。
vue-router
咱們以HTML5History這個類來看一下這個push方法,數組
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) }
push(咱們跳轉時的$router.push就是這個方法)過程當中調用了transitionTo完成了一系列的跳轉內容,但這個方法在HTML5的類中並不存在,繼承於base.js類中的方法
transitionTo就是實現路由跳轉的方法
transitionTo的主流程是由confirmTranstion方法於uodateRoute方法結合起來的,翻譯成普通話:路由跳轉要先通過一個確認跳轉的過程,在確認過程完成後進行一次路由的更新操做,緩存
transitionTo (location: RawLocation, onComplete?: Function, onAbort?: Function) { // 獲取要跳轉的而且通過處理的路由 const route = this.router.match(location, this.current) // confirmTranstion確認跳轉過程 this.confirmTransition(route, () => { // 確認完畢後完成更新路由操做 this.updateRoute(route) onComplete && onComplete(route) this.ensureURL() // fire ready cbs once if (!this.ready) { this.ready = true this.readyCbs.forEach(cb => { cb(route) }) } }, err => { if (onAbort) { onAbort(err) } if (err && !this.ready) { this.ready = true this.readyErrorCbs.forEach(cb => { cb(err) }) } }) }
confirmTransiton作了什麼呢?首先判斷一下你是否是相同的路由。若是是那就什麼都不作,第二步呢,咱們要開始收集一波守衛了,而後把守衛收集起來,而後把每一個守衛執行一遍,confirmTransition就算執行成功了。app
這個過程當中的難點是什麼?異步
在vue-router中封裝了一個runQueue函數來解決上面的三個問題的後兩個。第一個問題呢則涉及到vue-router處理路由的一個大篇章,咱們着重講一下runQueue函數async
1:迭代器模式來保證遍歷隊列時每一步都是可控的,函數
2:隊列完成後執行對應的回調函數,post
推斷出函數參數的對應功能:this
queue : 須要執行的守衛隊列
fn : 迭代器函數,守衛隊列的每個守衛都去執行迭代器函數
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) }
runQueue是怎麼幫助咱們解決守衛隊列處理的問題就算說完了。
處理守衛隊列的大錘子咱們已經制造好了,能夠開工了,那你的守衛隊列呢??
對對對,還有守衛隊列要收集。
這個時候咱們要想一想有哪些守衛?
前置守衛:
後置守衛:
咱們要想一下這些守衛都是怎麼註冊的,
beforeEach、beforeResolve、afterEach
beforeEnter
beforeRouteLeave、beforeRouteUpdate、beforeRouteEnter
好了咱們要去榨取對應的守衛了,
confirmTransition的守衛分爲兩個隊列:咱們先來看第一個隊列
// 拿到路由跳轉中更新、摧毀、激活時對應展現的組件。 const { updated, deactivated, activated } = resolveQueue(this.current.matched, route.matched) // 路由守衛 const queue: Array<?NavigationGuard> = [].concat( // in-component leave guards extractLeaveGuards(deactivated), // global before hooks this.router.beforeHooks, // in-component update hooks extractUpdateHooks(updated), // in-config enter guards activated.map(m => m.beforeEnter), // async components resolveAsyncComponents(activated) )
一個queue的順序:
7個守衛中的4個守衛都在被按順序拿出來了,放入第一個queue。
再下一步要有一個處理守衛的迭代器:
const iterator = (hook: NavigationGuard, next) => { if (this.pending !== route) { return abort() } try { hook(route, current, (to: any) => { // 傳個false就直接執行路由的錯誤處理,而後中止什麼都不作。 if (to === false || isError(to)) { // next(false) -> abort navigation, ensure current URL 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() // 咱們就執行路由跳轉操做,而且守衛隊列中止下面的迭代 if (typeof to === 'object' && to.replace) { this.replace(to) } else { this.push(to) } } else { // confirm transition and pass on the value // 接續迭代下去咯 next(to) } }) } catch (e) { abort(e) } }
next函數,以前在將runQueue的函數的時候,fn接收第二個參數(以前畫太重點),第二個參數的回調函數是完成迭代器向下一步執行的功能。
下面會有一點亂:
全部的前置守衛都接收三個參數
beforeEnter(to,from,next)=>{ //這個next就是咱們看到的 hook裏面接收的箭頭函數((to:any)=>{}) //這個箭頭函數裏面對迭代器的next進行了一下掉用, //保證在必定狀況下迭代器能夠向下走一步。 next('/index') // 咱們在這種next('/index')傳遞一個能夠執行的路徑時,(to:any)=>{} //這個箭頭函數並不會調用迭代的next,而是跳轉別的路徑執行了push操做。 // 若是咱們不掉用守衛中的next,迭代器的next確定並不會執行,守衛的迭代就中止了, // 守衛堵塞confirmTransition並不會執行完畢,也就不會由後面的更細路由操做了。 }
runQueue(queue, iterator, () => { const postEnterCbs = [] const isValid = () => this.current === route // wait until async components are resolved before // extracting in-component enter guards const enterGuards = extractEnterGuards(activated, postEnterCbs, isValid) const queue = enterGuards.concat(this.router.resolveHooks) runQueue(queue, iterator, () => { if (this.pending !== route) { return abort() } this.pending = null onComplete(route) if (this.router.app) { this.router.app.$nextTick(() => { postEnterCbs.forEach(cb => { cb() }) }) } }) })
咱們在把第一個queue(四個守衛與一個異步組件的加載)執行完畢後,要收集與執行第二個queue了,
第二個queue:
收集完開始執行第二個queue的迭代。第二個queue執行完執行一下onComplete函數,表明着confirmTransition方法執行完畢了。確認路由的過程結束了,
下面就是updateRoute的過程。updateRoute的時候執行所有的後置守衛,由於更新路由以後,當前的路由已經變化了,因此在給守衛傳參數的時候緩存了一下,以前的路由。
updateRoute (route: Route) { const prev = this.current this.current = route this.cb && this.cb(route) this.router.afterHooks.forEach(hook => { hook && hook(route, prev) }) }
因此爲何afterEach沒有next呢?由於afterEach根本不在迭代器以內,他就沒有next來觸發迭代器的下一步。
最後咱們說一下beforeEach的內容:
咱們設置beforeEach全局守衛的時候,守衛們存儲在哪裏?
beforeEach (fn: Function): Function { return registerHook(this.beforeHooks, fn) } function registerHook (list: Array<any>, fn: Function): Function { list.push(fn) // 返回值是一個function return () => { const i = list.indexOf(fn) if (i > -1) list.splice(i, 1) } }
這段代碼beforeEach是經過註冊守衛的方式,將註冊的全局前置守衛放在beforeHooks的容器內,這個容器裏面裝載着全部的前置守衛
一家人(全局的 前置進入、前置resolve、後置守衛)整整齊齊的放在對應的容器裏面,容器是個數組,因此註冊全局守衛的時候,是支持註冊多個的,
router.beforeEach(()=>{xxx}); router.beforeEach(()=>{yyy}); // 這兩個守衛都會執行,只是先註冊的先執行, // registerHook這個方法還能夠清除對應的守衛,這個方法也可使用
咱們來回答一下開篇的5個問題
beforeRouteLeave < beforeEach < beforeRouteUpdate < beforeEnter < beforeRouteEnter < beforeResolve < afterEach
next的做用,使導航守衛隊列的繼續向下迭代
afterEach根本不在導航守衛隊列內,沒有迭代的next
beforeEach是能夠疊加的,全部的全局前置守衛按順序存放在beforeHooks的數組裏面,
路由跳轉的核心方法是transitionTo,在跳轉過程當中經歷了一次confirmTransition,
(beforeRouteLeave < beforeEach < beforeRouteUpdate < beforeEnter < 異步組件加載)這樣順序的queue爲第一個,
在第一個queue迭代完畢後,執行第二個(beforeRouteEnter < beforeResolve)這樣順序的queue,
在執行完畢後,開始執行updateRoute,以後執行全局的afterEach守衛。最後完成路由的跳轉。