【VueRouter 源码学习】第十篇 - 全局钩子函数的实现

464 阅读4分钟

这是我参与8月更文挑战的第26天,活动详情查看:8月更文挑战


一,前言

上一篇,介绍了 router-view 组件的实现,主要涉及以下内容:

  • 函数式组件的介绍;
  • router-view 组件的实现:
    • 获取渲染记录;
    • 标记 router-view 层级深度;
    • 根据深度进行 router-view 渲染;

本篇,介绍 vue-router 全局钩子函数的实现;


二,完整的导航解析流程

路由钩子的渲染流程(完整的导航解析流程):

  1. 导航被触发。
  2. 在失活的组件里调用 beforeRouteLeave 守卫。
  3. 调用全局的 beforeEach 守卫。
  4. 在重用的组件里调用 beforeRouteUpdate 守卫 (2.2+)。
  5. 在路由配置里调用 beforeEnter。
  6. 解析异步路由组件。
  7. 在被激活的组件里调用 beforeRouteEnter。
  8. 调用全局的 beforeResolve 守卫 (2.5+)。
  9. 导航被确认。
  10. 调用全局的 afterEach 钩子。
  11. 触发 DOM 更新。
  12. 调用 beforeRouteEnter 守卫中传给 next 的回调函数,创建好的组件实例会作为回调函数的参数传入。

下文以 vue 中最常用的钩子:router.beforeEach 为例进行说明;


三,路由钩子的使用

通过 router.beforeEach 注册 beforeEach 钩子回调函数;

// router.js

router.beforeEach((from,to,next)=>{ 
  console.log(1);
  setTimeout(() => {
      next();
  }, 1000);
})

router.beforeEach((from,to,next)=>{ 
  console.log(2);
  setTimeout(() => {
      next();
  }, 1000);
})

同一个钩子可以多次进行注册,当触发执行时,会按照注册顺序依次执行对应函数;

使用这种写法,就可以按照不同功能进行逻辑隔离;比如:一个做权限控制,一个做动态路由;

观察钩子函数的使用方式:先多次注册,后依次调用;这就是类似于发布订阅模式;


四,路由钩子的实现

1,创建 router.beforeEach 方法 - 钩子函数的订阅

根据发布订阅模式:

  • 首先,需要在 router 实例上增加一个 beforeEach 方法;
  • VueRouter 实例化时,创建 beforeHooks 数组,用于存放注册的钩子函数;
  • 当执行 router.beforeEach 时,将钩子函数 push 到 beforeHooks 数组中,相当于订阅;
// index.js

class VueRouter {
    constructor(options) {  // 传入配置对象
        // 定义一个存放钩子函数的数组
        this.beforeHooks = [];
    }}
    // 在router.beforeEach时,依次执行注册的钩子函数
    beforeEach(fn){
        this.beforeHooks.push(fn);
    }
}
export default VueRouter;

2,beforeEach 钩子的执行时机

当路径切换时,需要让 beforeHooks 数组中注册的函数依次执行;

beforeEach 钩子的执行时机:路由已经开始切换,但还没有更新之前:

  • 在哪里做切换?base.js 中的 transitionTo 方法中进行切换;
  • 在哪里做更新?updateRoute 方法中进行赋值更新;

所以,beforeEach 钩子函数的代码执行位置:在 transitionTo 切换路由方法中,且在执行 updateRoute 方法之前;

// history/base.js

class History {
  constructor(router) {
    this.router = router;
  }

  /**
   * 路由跳转方法:
   *  每次跳转时都需要知道 from 和 to
   *  响应式数据:当路径变化时,视图刷新
   * @param {*}} location 
   * @param {*} onComplete 
   */
  transitionTo(location, onComplete) {
    let route = this.router.match(location);
    if (location == this.current.path && route.matched.length == this.current.matched.length) {
      return
    }

    // beforeEach的执行时机:
    // 在 transitionTo 切换路由方法中,且在执行 updateRoute 方法之前;
    
    this.updateRoute(route);
    onComplete && onComplete();
  }
}

export { History }

在更新之前调用注册好的导航守卫,执行完成后,执行 updateRoute 和 onComplete() 这两步逻辑;

3,执行注册的钩子函数

在 base.js 中,通过 this.router.beforeHooks 拿到 hook 数组(base.js中的History类中有 router 实例,而 beforeHooks 数组是声明在 router 实例上的)

由于 beforeHooks 可能存在多个函数,需要全部执行完成后,才可以继续执行后面的 updateRoute 和 onComplete() 这两步;

所以,需要一个执行全部回调函数的队列 runQueue:每次执行时,调用 iterator 迭代器方法,一个一个队列进行迭代,全部完成之后,继续执行 updateRoute 和 onComplete();

runQueue 的作用:将注册进来的钩子函数依次执行,并调用传入的 iterator;

runQueue 的核心思想是异步迭代;

// history/base.js

/**
 * 递归执行钩子函数
 * @param {*} queue 钩子函数队列
 * @param {*} iterator 执行钩子函数的迭代器
 * @param {*} cb 全部执行完成后调用
 */
function runQueue(queue, iterator, cb) {
  // 异步迭代
  function step(index) {
    // 结束条件:队列全部执行完成,执行回调函数 cb 更新路由
    if (index >= queue.length) return cb();
    let hook = queue[index]; // 先执行第一个 将第二个hook执行的逻辑当做参数传入
    iterator(hook, () => step(index + 1));
  }
  step(0);
}

将所有钩子函数拼接到一起,并通过 runQueue 队列执行所有函数:

// history/base.js

class History {
  constructor(router) {
    this.router = router;
  }

  /**
   * 路由跳转方法:
   *  每次跳转时都需要知道 from 和 to
   *  响应式数据:当路径变化时,视图刷新
   * @param {*}} location 
   * @param {*} onComplete 
   */
  transitionTo(location, onComplete) {
    let route = this.router.match(location);
    if (location == this.current.path && route.matched.length == this.current.matched.length) {
      return
    }
    // 获取到注册的回调方法 
    let queue = [].concat(this.router.beforeHooks); 
    const iterator = (hook, next) => {
      hook(this.current, route, () => {
        next();
      })
    }
    runQueue(queue, iterator, () => {
      // 将最后的两步骤放到回调中,确保执行顺序
      // 1,使用当前路由route更新current,并执行其他回调
      this.updateRoute(route);
      // 根据路径加载不同的组件  this.router.matcher.match(location)  组件 
      // 2,渲染组件
      onComplete && onComplete();
    })
  }
}

export { History }

五,结尾

本篇,介绍了全局钩子函数的实现,主要涉及以下内容:

  • 导航解析流程;
  • 路由钩子函数的使用和原理;
  • 路由钩子函数的实现;

下篇,待定;