在本期文章中,我将为你们梳理弄明白如下几个事情,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守卫。最后完成路由的跳转。