【4运作核心】路径切换和路由守卫

如果没有处理路径切换,整个路由器就没法运作,就跟没用一样。可见路径切换模块在其中的核心地位。

讲到“路径切换”,history.transitionTo是位于内部的核心。也就是绝大多数提供给用户的函数,最终都会执行到这里,开启路径切换的流程。

其间值得注意的是,利用迭代器设计模式,可简洁实现路由守卫,是非常巧妙的代码设计,会提前详细介绍。

到文章最后,文字描述路径切换的流程骨架。再剩余每个繁杂具体的 queue 钩子函数没有介绍,留给读者自行仔细研读。

history.transitionTo的架构地位:】

前言:history.transitionTo 是 Vue-Router 中非常重要的方法,当我们切换路由线路的时候,就会执行到该方法。

history.push、history.replace实际上也会执行history.transitionTo。函数里的this.confirmTrasition 完成一次真正的路径切换(也是当前话题下的重点)。

history.transitionTo的职责:】

(前一节我们分析了 matcher 的相关实现,知道它是如何找到匹配的新线路),那么匹配到新线路后发生的事,就是要研究history.transitionTo

其实路径切换函数是作为回调传入history.transitionTo,只要实现路径切换和在其前后调用路由守卫就行了。

【内部真正执行路径切换的函数】history.transitionTo内部会执行confirmTrasition,后者的代码才是真正去完成其职责。

“里面那么多不止用于路由守卫的钩子函数具体是怎么样实现处理相应事务的?” 这么枯燥,相信绝大多数人都不需要去关心。在这里先上一点干货,来点有意思的,最后才去贴枯燥的流程解释。

迭代器 设计模式 #敲黑板,划重点

我们知道路径切换的时候,会依次执行路由守卫钩子,同时路由守卫有“拦截”的作用。实际上 Vue-Router 在此时执行的钩子函数远不止这么少,只是很多内部的钩子函数没有暴露给用户而已。

“路由守卫”这个机制的实现,其实就是迭代器设计模式。在之后看源码的时候就会看到。

先来一个“最简版”迭代器模式,作为demo跑一下,发现设计得挺巧妙的:(其中注释掉的代码是可以去掉的,“注释代码”其作用就是等整个迭代器执行完之后再调用cbOfQueue而已)

function runQueue (queue, iterator, /* cbOfQueue */) {
  var step = function (index) {
    // if (index >= queue.length) {
      // cbOfQueue();
    // } else {
      if (queue[index]) {
        iterator(queue[index], function () {
          step(index + 1);
        });
      } else { step(index + 1); } // edge case:万一这下标没对应的数组元素
    // }
  };
  step(0);
}
async function iterator (fn, next) {
  await fn(); await /* 可以添加代码 */; next();
}

我们需要把要执行的各种“事务”(也就是回调函数)放入queue这个数组就行了。

我们看到iterator函数第2个参数是一个回调函数,我们称之为next,只要iterator执行了这个next回调,迭代器就能继续往下走。

而iterator函数在每一次循环中的职责就是把第1个传入参数(即其也是一个函数)执行掉,就能去执行next回调了。next本身就是一个() => step(index + 1),再通过下标来判断迭代器是否执行完毕即可。这就是满足实现迭代器的需要。

可见如果不执行next,迭代器就不能往下走了,这就是“路由守卫”的原理。(如果不想执行next就可以注入一个abort回调函数供执行以通知外界,这也是官方源码所实现的)

传入的钩子函数fn执行next回调之前,可以在fn中添加任意逻辑,这部分逻辑的载体又是一个回调函数,通常就是 Vue-Router 提供给用户自定义的钩子函数了。

如果能看懂代码,完全不需要看文字描述了!!!!!!!!!! “摘自源码的对应代码”如下,阅读难度稍稍提高了一点点:

// runQueue摘自源码原文:
/**
 * @param {Array<Function>} queue 将要执行的函数 的队列
 * @param {Function} iterator 传入 函数 给 iterator,iterator内可以执行函数,然后在合适的时候执行其回调(即step(index + 1)) 就能继续这个 runQueue。
 * @param {*} cbOfQueue 队列执行完毕后的回调
 */
function runQueue (queue, fn, cbOfQueue) {
  var step = function (index) {
    if (index >= queue.length) {
      cbOfQueue();
    } else {
      if (queue[index]) {
        fn(queue[index], function () { // fn === iterator
          step(index + 1);
        });
      } else { step(index + 1); } // edge case:万一这下标没对应的数组元素
    }
  };
  step(0);
}
// async function iterator (fn, next) {
//   await fn(); await /* 可以添加代码 */; next();
// }
// change to:
function iterator (fn, next) {
  fn(function cbOfFn() { // 如果想中间插入任务,就把fn弄成可以接收回调的,最后“延迟”到在回调中执行next即可
    next()
  })
};

经进一步理解之后,再把设计哲学上升一个高度:其实就是回调函数嵌套+哨兵变量 完成一个迭代器的逻辑。

好了,后面又是枯燥的“具体业务”解析环节:

传给 matcher.match 的 this.current #前置知识

this.currenthistory 维护的当前路径,它的初始值是在 history 的构造函数中初始化的。

this.current = START

START 的定义在 src/util/route.js 中:

export const START = createRoute(null, {
  path: '/'
})

这样就创建了一个初始的 Route,而 transitionTo 实际上也就是在切换 this.current,稍后我们会看到。

confirmTrasition 去做真正的路径切换且调用前后路由守卫

由于这个过程可能有一些异步的操作(如异步组件),所以整个 confirmTransition API 设计成带有成功回调函数和失败回调函数。

  • confirmTransition
    • (首先定义了 abort 函数)
    • 然后判断如果满足计算后的 routecurrent 是相同路径的话,则直接调用 this.ensureUrlabortensureUrl 这个函数我们之后会介绍。
    • 接着又根据 current.matchedroute.matched 执行了 resolveQueue 方法解析出 3 个队列
    • 3组钩子外加其他钩子放入到queue数组
    • runQueue(queue, iterator, callback) 迭代执行
resolveQueue:
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)
  }
}

因为 route.matched 是一个 RouteRecord 的数组,由于路径是由 current 变向 next,那么就遍历对比 2 边的 RouteRecord,找到一个不一样的位置 i

  • 那么 next 中从 0 到 iRouteRecord 是两边都一样,则为 updated 的部分;
  • i 到最后的 RouteRecordnext 独有的,为 activated 的部分;
  • current 中从 i 到最后的 RouteRecord 则没有了,为 deactivated 的部分。

拿到 updatedactivateddeactivated 3 个 ReouteRecord 数组后,接下来就是路径变换后的一个重要部分,执行一系列的钩子函数。

这个计算很简洁,但是有点巧妙,比较典型的例子 foo/bar 切换 到 foo: current是[{/*foo*/}, {/*bar*/}] ; 而 next 是 [{/*foo*/}]

反正就是3者算好之后,再和其他钩子放入到queue数组:
const queue: Array<?NavigationGuard> = [].concat(
  extractLeaveGuards(deactivated),
  this.router.beforeHooks,
  extractUpdateHooks(updated),
  activated.map(m => m.beforeEnter),
  resolveAsyncComponents(activated)
)

5步骤按照顺序如下:

  1. 在失活的组件里调用离开守卫。
  2. 调用全局的 beforeEach 守卫。
  3. 在重用的组件里调用 beforeRouteUpdate 守卫
  4. 在激活的路由配置里调用 beforeEnter
  5. 解析异步路由组件。
runQueue

NavigationGuard(to, from, next),原理参考前面已经提前介绍的“迭代器”设计模式,只是把变量名改改而已。

再看可自定义的iterator,源码的实现,会根据一些条件执行 abortnext

  var iterator = function (hook, next) {
    // 先不看pending相关逻辑
    try {
      hook(route, current, function (to) {
        if (to === false) {
          // next(false) -> abort navigation, ensure current URL
          this$1.ensureURL(true);
          abort(createNavigationAbortedError(current, route));
        } else if (isError(to)) {
          this$1.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(createNavigationRedirectedError(current, route));
          if (typeof to === 'object' && to.replace) {
            this$1.replace(to);
          } else {
            this$1.push(to);
          }
        } else {
          // confirm transition and pass on the value
          next(to);  // 其实就是 step(index + 1)
        }
      });
    } catch (e) {
      abort(e);
    }
  };

大的流程骨架讲完了。剩下的就是“一个一个放入queue中的每个导航守卫是怎么实现的”讲解。首先它们的书写格式,为了满足迭代器的需要,格式已经被限制死了。无非就是其间想实现什么业务的区别而已。

再讲到业务,其实笔者是有研读过导航守卫的具体内容的,这里面还有有一些有意思的代码,但是就算是你想实现相同的业务,这源码的可参考意义也不算很大,所以这里就不贴这些繁琐的代码了。所以本章节就在这里戛然结束了吧。

文章目录
  1. 迭代器 设计模式 #敲黑板,划重点
  2. 传给 matcher.match 的 this.current #前置知识
  3. confirmTrasition 去做真正的路径切换且调用前后路由守卫
    1. resolveQueue:
    2. 反正就是3者算好之后,再和其他钩子放入到queue数组:
    3. runQueue