likes
comments
collection
share

理解Koa洋葱模型,阅读50行串行Promise源代码

作者站长头像
站长
· 阅读数 46

本文参加了由公众号@若川视野 发起的每周源码共读活动,       点击了解详情一起参与。

这是源码共读的第5期 | koa-compose

目标

  1. 通过阅读 koa-compose 库,理解 洋葱圈模型的实现
  2. 使用Jest测试例子理解代码的调用流程
  3. 拓展设计模式 职责链模式

代码调试

代码仓库

这里推荐川哥的仓库 方便学习

git clone github.com/lxchuan12/k…

调试代码

先在 test/test.js 打下关键断点

理解Koa洋葱模型,阅读50行串行Promise源代码

找到package.json, 选择调试脚本,会执行对应的Npm脚本进入到我们的断点

理解Koa洋葱模型,阅读50行串行Promise源代码

进入断点后, 我们可以一步一步慢慢调试代码,使用以下几个关键按键

  1. F11 单步调试 也就是代码一步一步往下
  2. F10 单步跳过 执行代码 但是不会进入 比如函数当中的流程
  3. Shift+F11 单步跳出 从当前上下文跳出,如果你正在函数中 会跳出到函数外
  4. F5 继续 跳到下一个断点,如果你没有设置 下一个断点(就是给代码打上红色小点),那么结束调试 理解Koa洋葱模型,阅读50行串行Promise源代码

流程

整体结构

    function wait (ms) {
      return new Promise((resolve) => setTimeout(resolve, ms || 1))
    }
async () => {
    const arr = []
    const stack = []

    stack.push(async (context, next) => {
      arr.push(1)
      await wait(1)
      await next()
      await wait(1)
      arr.push(6)
    })

    stack.push(async (context, next) => {
      arr.push(2)
      await wait(1)
      await next()
      await wait(1)
      arr.push(5)
    })

    stack.push(async (context, next) => {
      arr.push(3)
      await wait(1)
      await next()
      await wait(1)
      arr.push(4)
    })

    await compose(stack)({})
}

先不管数组函数中context,next两个形参的作用, 可以先理解context 是执行的上下文作用域, 比如当它是一个对象的时候,用于传递或者使用里面的某些参数, next则是对应要调用的下一个函数

最后执行的时候 stack函数数组

compose函数

function compose (middleware) {
  if (!Array.isArray(middleware)) throw new TypeError('Middleware stack must be an array!')
  for (const fn of middleware) {
    if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
  }

  /**
   * @param {Object} context
   * @return {Promise}
   * @api public
   */

  return function (context, next) {
    let index = -1
    return dispatch(0)
    function dispatch (i) {
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i]
      if (i === middleware.length) fn = next
      if (!fn) return Promise.resolve()
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }
}

首先对middleware做了判断,主要是判断传递进来的是不是一个 所有成员为函数的数组参数。

函数执行完毕后 返回一个新的函数,这里涉及到高阶函数 以及闭包的知识, 所以前面对于compose的调用是两次调用 await compose(stack)({}), 第一次调用返回的就是这个函数,第二次调用就是执行这个返回的函数

dispatch函数

return function (context, next) {
    // last called middleware #
    let index = -1 //默认索引 -1
    return dispatch(0) //执行dispatch函数 传入默认索引为0
    // 定义dispatch函数 
    function dispatch (i) {
      //如果传入的索引 小于 index 说明 不存在下一个能调用的函数了
      //也是为了防止重复的 next调用
      if (i <= index) return Promise.reject(new Error('next() called multiple times'))
      index = i
      let fn = middleware[i] //得到对应数组索引中的 函数
      //这里的判断主要是看是不是到了最后一个函数了
      //如果是最后一个函数 则 调用外部传入的next函数 如果没传next函数 那就退出传递逻辑
      //这里就可以理解为 保底的尾部处理逻辑,对应职责链中的链尾处理
      if (i === middleware.length) fn = next 
      if (!fn) return Promise.resolve() //到了最后一个调用并且还没有链尾处理 则结束传递
      //如果还没到最后一个 则返回一个Promise 并执行下一个函数的调用
      try {
        return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
      } catch (err) {
        return Promise.reject(err)
      }
    }
  }

需要注意的是这里的 dispatch.bind(null, i + 1) 就是 dispatch函数的一个bind调用返回新函数,作用是为了下次调用传入的索引, 如果 Promise.resolve(fn(context, dispatch.bind(null, i + 1) 被调用,就会来到 之前 数组中定义的形参函数 并执行

理解Koa洋葱模型,阅读50行串行Promise源代码

context 就是一开始调用的时候 外部传入的一个空对象 对应这行代码的第二次调用await compose(stack)({}), 而next 就是 我们dispatch.bind(null, i + 1) 函数

此时执行起来就是:

  1. arr.push(1)
  2. await wait(1)
  3. await next() ←

当执行到 next() 函数时, 由于 索引已经改变了,第一次为0, 此时为1, 那么就会去调用对应的 数组中第二个成员的函数

理解Koa洋葱模型,阅读50行串行Promise源代码

以此类推,每当遇到next 函数 就会进入 数组 下一个成员的 函数中进行执行, 直到 最后一个成员时, 先看有没有 链尾函数,没有的话那么传递结束了。

理解Koa洋葱模型,阅读50行串行Promise源代码

最后,由于await的特性,会等待函数的执行 返回一个结果,那么此时函数会依次往上走并执行,流程就是这样:

理解Koa洋葱模型,阅读50行串行Promise源代码

这里会有点绕,重点是去理解 await 的特性, 它会等待 表达式后 对应函数的执行,此时它就变为同步代码了, 并且在await后的非await代码会被加入到 微任务队列当中

这样就变成了所谓的洋葱圈模型, 不得不说 这个设计 厉害。

理解Koa洋葱模型,阅读50行串行Promise源代码

最后附上经典的中间件gif示例图

理解Koa洋葱模型,阅读50行串行Promise源代码

总结

不得不说,虽然不到100行代码,但是涉及到的知识特别多,相当有难度。 我在理解了前面一期 p-limit之后, 并根据川哥推荐 的 Javascript设计模式与开发实践 阅读了职责链那一部分之后, 对于其中的理解就清晰了很多,也不得不感叹Koa巧妙的设计!

转载自:https://juejin.cn/post/7153547591987232804
评论
请登录