【源码】koa-compose洋葱模型原理解析---函数多层调用怎么写更优雅?
资料准备【若川】koa 洋葱模型实现:https://juejin.cn/post/7005375860509245471【函数式编程指北】:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.htmlhttps://www.yuque.com/docs/share/0268760e-60bf-4278-8
资料准备
- 【若川】koa 洋葱模型实现:https://juejin.cn/post/7005375860509245471
- 【函数式编程指北】:https://llh911001.gitbooks.io/mostly-adequate-guide-chinese/content/ch5.html
- https://www.yuque.com/docs/share/0268760e-60bf-4278-871e-c1e83a68be7a
学习目标
- 全程尝试调试并且记录
- 知道什么是koa洋葱模型
源码解析
git clone https://github.com/lxchuan12/koa-compose-analysis.git
cd koa-compose/compose
npm i
compose/index.js
'use strict'
/**
* Expose compositor.
* 导出 compose
*/
module.exports = compose
/**
* Compose `middleware` returning
* a fully valid middleware comprised
* of all those which are passed.
* @param {Array} middleware
* @return {Function}
* @api public
*
* 接收一个 是中间件 这个参数是数组, 并且每一项是函数
* 返回一个函数 接收`content` 和 `next`俩参数,最后返回一个promise
*/
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
* 返回一个函数 接收`content` 和 `next`俩参数,最后返回一个promise
*/
return function (context, next) {
// last called middleware #
// 默认执行 `dispatch(0)`
let index = -1
return dispatch(0)
function dispatch (i) {
// 一个函数不能多次调用
// 第一次 i 为 0 index 为 -1 可以继续走下去 此时index 为 0;
// 那么在走一遍 i此时还是0 index 也为0 i == index 抛出错误
if (i <= index) return Promise.reject(new Error('next() called multiple times'))
index = i
// 拿出数组中的每一项(获取中间件函数)
let fn = middleware[i]
// `next`是`undefined` 当相等的时候:`fn` 就是`undefined`
// 相等的时候相当于已经把`middleware`这个数组中最后一个已经拿出来了
if (i === middleware.length) fn = next
// 所以直接返回`promise` 的 `resolve`
if (!fn) return Promise.resolve()
/**
* 阅读到此时不太理解 `fn(context, dispatch.bind(null, i + 1))`
* 逐个解读:
* + `bind` 函数返回一个新函数
* + 参数1代表 `this`,如果函数不需要使用`this`,会写成`null`
* + 参数2就是要传的数据
* 在此处`i + 1` 对应的代码应该是 `let fn = middleware[i]` 为了获取`middleware`中下一个中间件函数
* 那么是否猜测可以理解为下一个 即`next`,`bind` 返回的是一个新函数
* 那么此时 是否可以理解为:
* 假设:中间件数组为[fn0, fn1, fn2]; (fn0 就是中间件【传入的参数数组】中的第一个函数)
* ```
* fn0(context, next) {
* return Promise.resolve(fn1(context,next) {
* return Promise.resolve(fn2(content, next) {
* //......一直到 `!fn`
* return Promise.resolve();
* // 此时也不再走 `next`函数
* })
* })
* }
* ```
* 但是一切是靠字面意思猜测,待调试的时候见真知
* 此时有没有发现,分析完竟然吧所有的中间件都串起来了,此时可以理解为这就是 洋葱模型
*/
try {
return Promise.resolve(fn(context, dispatch.bind(null, i + 1)))
} catch (err) {
return Promise.reject(err)
}
}
}
}
调试前去了解一下含义吧
什么是洋葱🧅模型
假如你手里有一支牙签,横向穿过一个洋葱,是不是会层层穿透?从第一层进去、到第二层、第三次…然后到中间层后,再层层穿透的出,从第三层出、第二层、第一层…
这就是洋葱模型,就如分析的上方代码一样将中间件一个个获取出来。
初步了解了含义,那么就调试来感受吧
- 找到
koa-compose-analysis/compose/test/test.js
- 1.继续(F5): 点击后代码会直接执行到下一个断点所在位置,如果没有下一个断点,则认为本次代码执行完成。
- 2.单步跳过(F10):点击后会跳到当前代码下一行继续执行,不会进入到函数内部。
- 3.单步调试(F11):点击后进入到当前函数的内部调试,比如在 compose 这一行中执行单步调试,会进入到 compose 函数内部进行调试。
- 4.单步跳出(Shift + F11):点击后跳出当前调试的函数,与单步调试对应。
- 5.重启(Ctrl + Shift + F5):顾名思义。
- 6.断开链接(Shift + F5):顾名思义。
- 45行处调用了
compose
- 进入
compose
这个函数中。可以看到,传入的参数 , 与上方看的源码的意思一样,继续往下看~
compose
函数的流程如下:- 1.验证
middleware
参数是否是一个数组 - 2.验证
middleware
参数里面的每一个元素是否为一个函数 - 3.返回一个函数,接受两个参数,一个
context
, 一个next
- 走到从此判断,返回了一个函数5. 接着点击继续 就跳到了此处,那么重新进入此函数看看接下来是怎么运作的~
- 此时进入了
dispatch
,dispatch
主要干的事情就是:
- 1.判断函数不能多次调用,
- 2.更新
index
的值 - 3.拿出中间件的每一项值,赋值给
fn
- 4.直到判断出
fn
是undeifined
,返回一个resolve
的promise
- 5.否则调用
fn
,context
值为最初的context
,next
为dispatch.bind(null, i + 1)
,也就是继续调用dispath
方法,参数为i+1
- 6.这里面注意一下,给bind传第一参数null, 函数内的this会指向默认宿主对象。
- 当i=0时,
fn
指向middleware
数组中的第1个元素
继续往下走,就在截图此处打一个断点
当i=1时,fn
指向middleware
数组中的第2个元素
当i=2的时候的情况:
当i=3的时候,fn为undefined的情况:
再来看看这块:
这块的代码输出是[1,2,3,4,5,6],
为什么呢?
答案就是:加载完所有中间件后,输出[1,2,3],调用next()后执行完当前中间件,然后把执行权交给上一层中间件。借用一张非常经典的图。
为了方便理解回忆在看一个 koa
的demo
const Koa = require('koa');
const app = new Koa();
const PORT = 3000;
// #1
app.use(async (ctx, next)=>{
console.log(1)
await next();
console.log(1)
});
// #2
app.use(async (ctx, next) => {
console.log(2)
await next();
console.log(2)
})
app.use(async (ctx, next) => {
console.log(3)
})
app.listen(PORT);
console.log(`http://localhost:${PORT}`);
1
2
3
2
1
测试技巧
- 在it后面加上一个
only
来只执行这一个测试
it.only('should work', async () => {})
- 在it后面加上一个
skip
来跳过这个测试
it.skip('should work', async () => {})
- 继续(F5): 点击后代码会直接执行到下一个断点所在位置,如果没有下一个断点,则认为本次代码执行完成。
- 单步跳过(F10):点击后会跳到当前代码下一行继续执行,不会进入到函数内部。
- 单步调试(F11):点击后进入到当前函数的内部调试,比如在 compose 这一行中执行单步调试,会进入到 compose 函数内部进行调试。
- 单步跳出(Shift + F11):点击后跳出当前调试的函数,与单步调试对应。
- 重启(Ctrl + Shift + F5):顾名思义。
- 断开链接(Shift + F5):顾名思义。
现实中函数多层调用时的处理方法
- 知识点:在函数式编程当中有一个很重要的概念就是
函数组合
, 实际上就是把处理数据的函数像管道一样连接起来, 然后让数据穿过管道得到最终的结果。
多层函数嵌套的运行结果,即把前一个函数的运行结果赋值给后一个函数。但是如果需要嵌套多层函数,那这种类似于
f(g(h(x)))
的写法可读性太差,我们考虑能不能写成(f, g, h)(x)
这种简单直观的形式,于是compose()
函数就正好帮助我们实现。compose的缺点:不能直观的看到参数
function getId(id) {
// 一些处理
// console.log(id, 'id1');
return id
}
function getData(id) {
// 一些处理
// console.log(id, 'id2');
let data = id * 10;
return data
}
function formatData(data) {
// 一些处理
// console.log(data, 'data')
let formatdata = data / 2;
// console.log(formatdata, 'formatdata')
return formatdata
}
const result = formatData(getData(getId(5)))
console.log(result)
//用compose 思想
const arr = [getId, getData, formatData];
const result = compose(arr)(3);
console.log(result, 'result') // 15,result
简易
function compose(funcs) {
if (!Array.isArray(funcs)) throw new TypeError('Middleware stack must be an array!')
// 遍历这个参数数组
for (const fn of funcs) {
// 校验数组中的每一项是不是函数
if (typeof fn !== 'function') throw new TypeError('Middleware must be composed of functions!')
}
return function (...args) {
//=>args:第一次调用函数传递的参数集合
let len = funcs.length;
if (len === 0) {
//=>一个函数都不需要执行,直接返回参数args
return args;
}
if (len === 1) {
//=>只需要执行第一个函数,把函数执行,把其结果返回即可
return funcs[0](...args);
}
return funcs.reduce((x, y) => {
// console.log('--x--', x)
// console.log('--y--', y)
return typeof x === "function" ? y(x(...args)) : y(x)
});
};
}
总结归纳
- 本次很完整的进行了调试,以前都是
console
,现在第一反应就是调试。 - 通过尝试写例子,对这次的知识点了解更深
- 写笔记跟看过就是俩回事啊,在我这里,不写真的等于不会
- 我写例子调试的网站jsRun
- 坚持就是胜利!
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)