Vue Router路由守卫流程解析

1,679 阅读2分钟

看Vue Router的官方文档,会发现这样一句话:

守卫是异步解析执行,此时导航在所有守卫 resolve 完之前一直处于 等待中

从这句话可以提出两个关键点:

  1. 异步解析,说明等异步完成后才己写往下执行
  2. 所有守卫都被resolve了,导航才会变 除了上面两点外,笔者经常烦恼路由守卫的执行顺序,当路由切换时,路由守卫的顺序是如何的呢?
    还想弄明白,路由组件是如何确定被复用的呢?

那在Vue Router中是如何实现的呢?让我们一探究竟吧👉👉👉
👋P.S. 欢迎评论,点赞,分享,收藏~
👋P.S. 如有不对,欢迎指正~

异步解析

在路由守卫的回调中执行异步操作,会等这个异步结束后,再执行下一个守卫,能这样做的关键是,调用了next方法,来resolve当前的钩子,执行下一个钩子。

举个例子: image.png image.png

这说明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才会去变化。 image.png 接下来分析一下整个路由被resolve的过程,调用push方法后,会执行transitionTo方法,里面调用router上的match方法,解析出要跳转到的路由对象route,之后调用confirmTransition image.png

第二步,confirmTransition函数中,通过对比要跳转到的路由对象当前的路由对象,获取哪些路由组件要被update,哪些被匹配上了,哪些要被移除。 image.png

进入resolveQueue方法看看👉 image.png

第三步,按顺序组织路由守卫数组,把上步得到的路由组件的状态传入相关的路由守卫提取方法中 image.png 到这里数组还差,组件内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方法的参数做状态判断,从而实现下图的功能, image.png

官网给的应用场景👉: image.png

第五步,开始执行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=>{})回调; image.png

放张在守卫中使用this的截图,这块和守卫数组执行过程息息相关,要想获取this,那必须在当前路由组件被确定后。 image.png

第六步,执行回调,最终确定导航, 执行开发者this.$router.push(location, onComplate, onAbort)方法的回调。 image.png

完整的导航解析流程

image.png References: router.vuejs.org/zh/guide/ad…

router上的push和replace方法,返回Promise

image.png

Vue3中实现runQueue的方式

用Array.prototype上的reduce方法来循环数组,借助Promise的链式调用来推进路由守卫

function runGuardQueue(guards) {
    return guards.reduce((promise, guard) => promise.then(() => guard()), Promise.resolve());
}