看Vue Router的官方文档,会发现这样一句话:
守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中
从这句话可以提出两个关键点:
- 异步解析,说明等异步完成后才己写往下执行
- 所有守卫都被resolve了,导航才会变
除了上面两点外,笔者经常烦恼路由守卫的执行顺序,当路由切换时,路由守卫的顺序是如何的呢?
还想弄明白,路由组件是如何确定被复用的呢?
那在Vue Router中是如何实现的呢?让我们一探究竟吧👉👉👉
👋P.S. 欢迎评论,点赞,分享,收藏~
👋P.S. 如有不对,欢迎指正~
异步解析
在路由守卫的回调中执行异步操作,会等这个异步结束后,再执行下一个守卫,能这样做的关键是,调用了next方法,来resolve当前的钩子,执行下一个钩子。
举个例子:
这说明next方法在Vue Router中起着开拓者的作用,有它才会推着路由逐步解析下去。
路由守卫过程
在源码中,所有的导航首位按着顺序被存放在一个数组中,当切换路由时(e.g. 调用router上的push或者replace方法时),会根据location的path和你的router配置,生成新的matched数组,然后对比新旧两个matched数组,找出哪些路由组件被复用了,哪些是新match的路由组件,哪些路由组件已经不再匹配了,之后根据这些matched的信息,去提取路由守卫,按这样组件内beforeRouteLeave守卫,全局beforeEach守卫,组件内beforeRouteUpate守卫,路由独享beforeEnter守卫,异步路由解析,组件内beforeRouteEnter守卫,全局beforeResolve守卫,全局afterEach守卫的顺序存放在数组中,然后按顺序执行数组,没当执行完一个守卫,就会调用next方法,把进度往下推进,直到数组被执行完毕。
上面说的有点概括了,下面分步说一下~
第一步,调用this.$router.push({name: 'user', params: {id: 1}}), 注意什么时候执行transitionTo的回调,什么时候导航被确定,因为在回调中执行pushState方法,url才会去变化。
接下来分析一下整个路由被resolve的过程,调用push方法后,会执行transitionTo方法,里面调用router上的match方法,解析出要跳转到的路由对象route,之后调用confirmTransition
第二步,confirmTransition函数中,通过对比要跳转到的路由对象和当前的路由对象,获取哪些路由组件要被update,哪些被匹配上了,哪些要被移除。
进入resolveQueue方法看看👉
第三步,按顺序组织路由守卫数组,把上步得到的路由组件的状态传入相关的路由守卫提取方法中
到这里数组还差,
组件内beforeRouteEnter,全局路由解析钩子和全局后置钩子呢,那这些钩子什么时候被加入守卫数组呢?需要等异步组件解析完毕了,后面第五步会说到哦~
第四步,现在分析一下执行守卫数组这个过程,首先会去调用runQueue方法,然后每执行完数组中的一项,就调用iterator方法,执行数组的下一项。这里把源码简化了一下,把大致流程展示出来,其实就如迭代器一下,一个个执行。
// queue就是守卫数组, 那queue[i], 就是路由守卫hook,
// 守卫hook是一个方法, 该方法给开发者来定义,vue router会给该方法注入3个参数,即to, from,next
// fn就是iterator方法, 它的回调用于推进守卫数组执行下一项
// cb是整个守卫数组执行完毕后的回调
const runQueue = (queue, fn, cb)=>{
const step = (index)=>{
if (index >= queue.length) {
cb()
} else {
if (queue[index]) {
fn(queue[index], ()=>{
step(index+1)
})
} else {
step(index+1)
}
}
}
step(0)
}
const iterator = (hook, next)=>{
hook(to, from, (to)=>{
// 当给next方法中传递了参数,会根据情况执行不同的代码,见下图👉
// 这块不是本文重点,所以先忽略
// 直接看执行next()方法
next()
})
}
👉iterator方法中,要对next方法的参数做状态判断,从而实现下图的功能,
官网给的应用场景👉:
第五步,开始执行runQueue,这块主要看runqueue的传参,第一个是守卫数组,第二个是iterator方法,第三个是runQueue最终回调,注意看注释哦
// -----------------调用 runQueue---------------P.S.这下面有点伪代码
runQueue([beforeRouteLeave, beforeEach, beforeRouteUpdate, beforeEnter, resolveAsyncComponents], iterator, ()=>{
var postEnterCbs = [];
var isValid = function () { return this$1.current === route; };
// 这时解析完了异步组件,开始提取其组件内enter守卫,和全局的beforeResolve守卫
// 提取完后,再次调用runQueue开始执行[beforeRouteEnter, beforeResolve]
// wait until async components are resolved before
// extracting in-component enter guards
var enterGuards = extractEnterGuards(activated, postEnterCbs, isValid);
var queue = enterGuards.concat(this$1.router.resolveHooks);
runQueue(queue, iterator, function () {
if (this$1.pending !== route) {
return abort()
}
this$1.pending = null;
// 在执行onComplete(route)时,会触发updateRoute方法,里面会去调afterEach全局后置守卫
onComplete(route);
// 这里还有一个postEnterCbs, 这个是对beforeRouteEnter hook中写的next(vm=>{})的回调做处理,
// 由于是$nextTick的回调中,所以这时已经mounted了,可以获取组件当前的this实例了
if (this$1.router.app) {
this$1.router.app.$nextTick(function () {
postEnterCbs.forEach(function (cb) { cb(); });
});
}
});
})
// onComplete(route)执行的其实是confirmTransition方法的回调,里面会调用updateRoute方法
// 除此之外,confirmTransition方法的回调中还会调用transitionTo的回调,就回到了第一步说的,
// 当调用transitionTo回调时,导航才发生跳转
History.prototype.updateRoute = function updateRoute (route) {
var prev = this.current;
this.current = route;
this.cb && this.cb(route);
// afterHooks在这里
this.router.afterHooks.forEach(function (hook) {
hook && hook(route, prev);
});
};
截个runQueue的源码,再大致看一下,需要注意上面提到的三点哦,
- 当异步组件被解析完毕后,会去调用其组件内的enter钩子,和全局路由解析钩子(beforeResolve), 那会被concat到当前守卫数组中;
- 执行afterEach守卫;
- 执行beforeRouteEnter中next(vm=>{})回调;
放张在守卫中使用this的截图,这块和守卫数组执行过程息息相关,要想获取this,那必须在当前路由组件被确定后。
第六步,执行回调,最终确定导航, 执行开发者this.$router.push(location, onComplate, onAbort)方法的回调。
完整的导航解析流程
References: router.vuejs.org/zh/guide/ad…
router上的push和replace方法,返回Promise
Vue3中实现runQueue的方式
用Array.prototype上的reduce方法来循环数组,借助Promise的链式调用来推进路由守卫
function runGuardQueue(guards) {
return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
}