理解Koa洋葱模型,阅读50行串行Promise源代码
本文参加了由公众号@若川视野 发起的每周源码共读活动, 点击了解详情一起参与。
这是源码共读的第5期 | koa-compose
目标
- 通过阅读
koa-compose
库,理解 洋葱圈模型的实现 - 使用
Jest
测试例子理解代码的调用流程 - 拓展设计模式
职责链模式
代码调试
代码仓库
这里推荐川哥的仓库 方便学习
git clone github.com/lxchuan12/k…
调试代码
先在 test/test.js
打下关键断点
找到package.json
, 选择调试脚本,会执行对应的Npm脚本进入到我们的断点
进入断点后, 我们可以一步一步慢慢调试代码,使用以下几个关键按键
- F11 单步调试 也就是代码一步一步往下
- F10 单步跳过 执行代码 但是不会进入 比如函数当中的流程
- Shift+F11 单步跳出 从当前上下文跳出,如果你正在函数中 会跳出到函数外
- F5 继续 跳到下一个断点,如果你没有设置 下一个断点(就是给代码打上红色小点),那么结束调试
流程
整体结构
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)
被调用,就会来到 之前 数组中定义的形参函数 并执行
context
就是一开始调用的时候 外部传入的一个空对象 对应这行代码的第二次调用await compose(stack)({})
, 而next
就是 我们dispatch.bind(null, i + 1)
函数
此时执行起来就是:
- arr.push(1)
- await wait(1)
- await next() ←
当执行到 next()
函数时, 由于 索引已经改变了,第一次为0, 此时为1, 那么就会去调用对应的 数组中第二个成员的函数
以此类推,每当遇到next
函数 就会进入 数组 下一个成员的 函数中进行执行, 直到 最后一个成员
时, 先看有没有 链尾函数,没有的话那么传递结束了。
最后,由于await的特性
,会等待函数的执行 返回一个结果,那么此时函数会依次往上走并执行,流程就是这样:
这里会有点绕,重点是去理解 await 的特性, 它会等待 表达式后 对应函数的执行,此时它就变为同步代码了, 并且在await后的非await代码会被加入到 微任务队列当中
这样就变成了所谓的洋葱圈模型
, 不得不说 这个设计 厉害。
最后附上经典的中间件gif示例图
总结
不得不说,虽然不到100行代码,但是涉及到的知识特别多,相当有难度。 我在理解了前面一期 p-limit
之后, 并根据川哥推荐 的 Javascript设计模式与开发实践
阅读了职责链
那一部分之后, 对于其中的理解就清晰了很多,也不得不感叹Koa巧妙的设计!
转载自:https://juejin.cn/post/7153547591987232804