【源码】koa-compose洋葱模型原理解析---函数多层调用怎么写更优雅?

本文主要是介绍【源码】koa-compose洋葱模型原理解析---函数多层调用怎么写更优雅?,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

资料准备
  • 【若川】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 = -1return 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)}}}
}
调试前去了解一下含义吧

在这里插入图片描述

什么是洋葱🧅模型

假如你手里有一支牙签,横向穿过一个洋葱,是不是会层层穿透?从第一层进去、到第二层、第三次…然后到中间层后,再层层穿透的出,从第三层出、第二层、第一层…
这就是洋葱模型,就如分析的上方代码一样将中间件一个个获取出来。

初步了解了含义,那么就调试来感受吧
  1. 找到 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):顾名思义。
  1. 45行处调用了compose 在这里插入图片描述
  2. 进入compose这个函数中。可以看到,传入的参数 , 与上方看的源码的意思一样,继续往下看~
  • compose函数的流程如下:
  • 1.验证middleware参数是否是一个数组
  • 2.验证middleware参数里面的每一个元素是否为一个函数
  • 3.返回一个函数,接受两个参数,一个context, 一个next
    在这里插入图片描述
  1. 走到从此判断,返回了一个函数在这里插入图片描述5. 接着点击继续 就跳到了此处,那么重新进入此函数看看接下来是怎么运作的~在这里插入图片描述
  2. 此时进入了dispatchdispatch主要干的事情就是:
  • 1.判断函数不能多次调用,
  • 2.更新 index的值
  • 3.拿出中间件的每一项值,赋值给fn
  • 4.直到判断出fnundeifined ,返回一个resolvepromise
  • 5.否则调用fn, context值为最初的context, nextdispatch.bind(null, i + 1),也就是继续调用dispath方法,参数为i+1
  • 6.这里面注意一下,给bind传第一参数null, 函数内的this会指向默认宿主对象。在这里插入图片描述
  1. 当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()后执行完当前中间件,然后把执行权交给上一层中间件。借用一张非常经典的图。
在这里插入图片描述

为了方便理解回忆在看一个 koademo

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) {//=>一个函数都不需要执行,直接返回参数argsreturn 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
  • 坚持就是胜利!

这篇关于【源码】koa-compose洋葱模型原理解析---函数多层调用怎么写更优雅?的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/351409

相关文章

Python调用Orator ORM进行数据库操作

《Python调用OratorORM进行数据库操作》OratorORM是一个功能丰富且灵活的PythonORM库,旨在简化数据库操作,它支持多种数据库并提供了简洁且直观的API,下面我们就... 目录Orator ORM 主要特点安装使用示例总结Orator ORM 是一个功能丰富且灵活的 python O

Java调用DeepSeek API的最佳实践及详细代码示例

《Java调用DeepSeekAPI的最佳实践及详细代码示例》:本文主要介绍如何使用Java调用DeepSeekAPI,包括获取API密钥、添加HTTP客户端依赖、创建HTTP请求、处理响应、... 目录1. 获取API密钥2. 添加HTTP客户端依赖3. 创建HTTP请求4. 处理响应5. 错误处理6.

0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型的操作流程

《0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeekR1模型的操作流程》DeepSeekR1模型凭借其强大的自然语言处理能力,在未来具有广阔的应用前景,有望在多个领域发... 目录0基础租个硬件玩deepseek,蓝耘元生代智算云|本地部署DeepSeek R1模型,3步搞定一个应

Python itertools中accumulate函数用法及使用运用详细讲解

《Pythonitertools中accumulate函数用法及使用运用详细讲解》:本文主要介绍Python的itertools库中的accumulate函数,该函数可以计算累积和或通过指定函数... 目录1.1前言:1.2定义:1.3衍生用法:1.3Leetcode的实际运用:总结 1.1前言:本文将详

Deepseek R1模型本地化部署+API接口调用详细教程(释放AI生产力)

《DeepseekR1模型本地化部署+API接口调用详细教程(释放AI生产力)》本文介绍了本地部署DeepSeekR1模型和通过API调用将其集成到VSCode中的过程,作者详细步骤展示了如何下载和... 目录前言一、deepseek R1模型与chatGPT o1系列模型对比二、本地部署步骤1.安装oll

Spring AI Alibaba接入大模型时的依赖问题小结

《SpringAIAlibaba接入大模型时的依赖问题小结》文章介绍了如何在pom.xml文件中配置SpringAIAlibaba依赖,并提供了一个示例pom.xml文件,同时,建议将Maven仓... 目录(一)pom.XML文件:(二)application.yml配置文件(一)pom.xml文件:首

一分钟带你上手Python调用DeepSeek的API

《一分钟带你上手Python调用DeepSeek的API》最近DeepSeek非常火,作为一枚对前言技术非常关注的程序员来说,自然都想对接DeepSeek的API来体验一把,下面小编就来为大家介绍一下... 目录前言免费体验API-Key申请首次调用API基本概念最小单元推理模型智能体自定义界面总结前言最

轻松上手MYSQL之JSON函数实现高效数据查询与操作

《轻松上手MYSQL之JSON函数实现高效数据查询与操作》:本文主要介绍轻松上手MYSQL之JSON函数实现高效数据查询与操作的相关资料,MySQL提供了多个JSON函数,用于处理和查询JSON数... 目录一、jsON_EXTRACT 提取指定数据二、JSON_UNQUOTE 取消双引号三、JSON_KE

MySql死锁怎么排查的方法实现

《MySql死锁怎么排查的方法实现》本文主要介绍了MySql死锁怎么排查的方法实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧... 目录前言一、死锁排查方法1. 查看死锁日志方法 1:启用死锁日志输出方法 2:检查 mysql 错误

MySQL数据库函数之JSON_EXTRACT示例代码

《MySQL数据库函数之JSON_EXTRACT示例代码》:本文主要介绍MySQL数据库函数之JSON_EXTRACT的相关资料,JSON_EXTRACT()函数用于从JSON文档中提取值,支持对... 目录前言基本语法路径表达式示例示例 1: 提取简单值示例 2: 提取嵌套值示例 3: 提取数组中的值注意