洋葱模型
洋葱模型(Onion Model)是中间件执行的核心逻辑模式,特点是请求先逐层穿过外层中间件到达核心逻辑,再反向逐层穿过外层中间件返回,形似洋葱的层级结构。每个中间件既可以处理 “进入” 阶段的逻辑(如前置校验),也可以处理 “返回” 阶段的逻辑(如结果处理)。
假设存在两个中间件 A 和 B,以及核心逻辑 C,执行顺序如下:进入 A → 进入 B → 执行 C → 离开 B → 离开 A 整个流程像剥洋葱一样,从外层到内层,再从内层回到外层。
代码实现洋葱模型
executeOnion 函数使用递归和闭包实现了中间件的 “逐层进入、逐层返回” 逻辑,核心是 next() 函数的闭包特性—— 它能记住当前执行到的中间件索引(index),每次调用 next() 就 “推进” 到下一个中间件,直到所有中间件执行完毕后触发核心逻辑。
-
executeOnion 函数是洋葱模型的核心,它接受三个参数:middlewares 中间件数组,core 核心逻辑,ctx 全局上下文对象,然后就是记录index和执行next函数
-
index 是 executeOnion 函数作用域内的变量,next() 作为闭包能持续访问和修改它 —— 每次调用 next(),index 就会递增,确保中间件按顺序执行
-
next 函数是每次调用都“消费”一个中间件,每次调用 next(),index 就会递增,确保中间件按顺序执行,当 index 达到中间件长度时,执行核心逻辑, 核心逻辑 core 是最后一个中间件,当所有中间件执行完后,执行核心逻辑
- 每个中间件是一个函数,常用参数是
ctx 和 next,ctx 是用于在中间件之间、中间件与核心逻辑之间传递数据(如请求参数、状态、结果),避免使用全局变量,保证数据隔离;next 用于触发下一个中间件或核心逻辑,是实现 “逐层进入、逐层返回” 的关键。中间件参数本质是解决数据共享和流程串联。
-
await next() 的核心作用是启动整个洋葱流程,index 的递增是由 next() 函数内部的逻辑完成的。await next()是 “触发按钮”,而 index 递增是 “按钮按下后内部的机械动作”—— 按钮本身不直接推动 index,但按下按钮会触发推动 index 的逻辑。
// 洋葱模型执行器:递归串联中间件和核心逻辑
const executeOnion = async (middlewares, core, ctx) => {
let index = 0; // 记录当前执行到的中间件下标(闭包变量),
// 定义next函数:每次调用都“消费”一个中间件,next函数是一个async函数,所以可以await下一个中间件或核心逻辑
const next = async () => {
if (index < middlewares.length) {
// 1. 取出当前下标对应的中间件
const currentMiddleware = middlewares[index];
// 2. 下标+1,为下一次调用next()做准备
index++;
// 3. 执行当前中间件,并把ctx和next传给它
await currentMiddleware(ctx, next);
} else {
// 4. 所有中间件执行完后,执行核心逻辑
await core(ctx);
}
};
// 启动:第一次调用next(),开始执行第一个中间件
await next();
};
// 定义中间件数组
const middlewares = [
// 中间件1:日志记录
async (ctx, next) => {
console.log('中间件1 - 进入');
await next(); // 执行下一个中间件/核心逻辑
console.log('中间件1 - 离开');
},
// 中间件2:耗时统计
async (ctx, next) => {
const start = Date.now();
console.log('中间件2 - 进入');
await next(); // 执行下一个中间件/核心逻辑
const end = Date.now();
console.log(`中间件2 - 离开,耗时:${end - start}ms`);
},
];
// 核心逻辑
const coreLogic = async (ctx) => {
console.log('执行核心逻辑');
ctx.result = '核心逻辑结果';
};
// 测试执行
const ctx = {};
executeOnion(middlewares, coreLogic, ctx).then(() => {
console.log('最终结果:', ctx.result);
});
// 输出:
// 中间件1 - 进入
// 中间件2 - 进入
// 执行核心逻辑
// 中间件2 - 离开,耗时:xms
// 中间件1 - 离开
// 最终结果:核心逻辑结果
详细的执行过程
// 初始状态:index = 0
await next(); // 第一次调用 next()
// next() 内部执行:
// 1. index=0 < 2 → 取出中间件1
// 2. index++ → index=1
// 3. 执行中间件1:console.log('中间件1进') → 调用 await next()(第二次调用 next())
// 第二次调用 next() 内部:
// 1. index=1 < 2 → 取出中间件2
// 2. index++ → index=2
// 3. 执行中间件2:console.log('中间件2进') → 调用 await next()(第三次调用 next())
// 第三次调用 next() 内部:
// 1. index=2 ≥ 2 → 执行核心逻辑
// 2. 核心逻辑执行完,回到中间件2的 await next() 之后 → console.log('中间件2出')
// 3. 中间件2执行完,回到中间件1的 await next() 之后 → console.log('中间件1出')
// 4. 中间件1执行完,回到第一次 await next() 之后 → 整个流程结束
深度理解中间件的代码,把中间件 1 的代码拆成三步看:
async (ctx, next) => {
// 第一步:进入中间件1,先执行“进入”逻辑
console.log('中间件1 - 进入');
// 第二步:调用next(),触发后续所有逻辑(中间件2 → 核心逻辑)
// 这里的await会“暂停”中间件1的执行,直到next()对应的Promise完成
await next();
// 第三步:只有等next()的所有后续逻辑执行完,才会走到这里
console.log('中间件1 - 离开');
};
关键:await next() 的 “暂停 - 恢复” 机制
- 暂停:当执行到
await next() 时,中间件 1 的执行会暂停,JavaScript 引擎会去执行 next() 指向的逻辑(中间件 2);
- 递归触发:中间件 2 里也有
await next(),会继续暂停中间件 2,触发核心逻辑;
- 恢复:核心逻辑执行完后,中间件 2 的
await next() 完成,继续执行中间件 2 的后续代码(“中间件 2 - 离开”);中间件 2 执行完后,中间件 1 的 await next() 才完成,继续执行中间件 1 的后续代码(“中间件 1 - 离开”)。
把中间件的执行逻辑用嵌套函数模拟,会更直观:
// 模拟中间件1的执行
const middleware1 = async () => {
console.log('中间件1 - 进入');
// 模拟await next():执行中间件2
await middleware2();
console.log('中间件1 - 离开');
};
// 模拟中间件2的执行
const middleware2 = async () => {
console.log('中间件2 - 进入');
// 模拟await next():执行核心逻辑
await coreLogic();
console.log('中间件2 - 离开');
};
// 模拟核心逻辑
const coreLogic = async () => {
console.log('执行核心逻辑');
};
// 启动执行
middleware1();
await next() 就像 “打开一扇门进入内层”,只有等内层的所有事情(后续中间件、核心逻辑)全部办完,门才会关上,回到当前中间件继续执行后续代码。这也是为什么中间件的 “离开” 逻辑会按反向顺序执行 —— 内层逻辑必须先完成,外层才能收尾。
考验环节
- 请用一句话概括「洋葱模型」的核心执行逻辑,并用通俗的例子解释它的应用场景?
- 洋葱模型中,next() 函数的核心作用是什么?如果某个中间件里不调用 next(),会发生什么?
已知以下中间件数组和核心逻辑,结合我们之前写的 executeOnion 执行器:
const middlewares = [
async (ctx, next) => {
console.log('A 进');
ctx.msg = 'A';
await next();
console.log('A 出');
},
async (ctx, next) => {
console.log('B 进');
ctx.msg += 'B';
await new Promise((resolve) => setTimeout(resolve, 1000)); // 模拟异步
await next();
console.log('B 出');
},
async (ctx, next) => {
console.log('C 进');
ctx.msg += 'C';
await next();
console.log('C 出');
},
];
const coreLogic = async (ctx) => {
console.log('核心逻辑');
ctx.msg += '核心';
};
-
请写出最终的控制台输出顺序(包括耗时相关日志)?
-
执行完后,ctx.msg 的值是什么?
-
如果把中间件 B 的 await next() 改成 next()(去掉 await),输出顺序会发生什么变化?为什么?
-
请基于洋葱模型,实现一个简化版的「Zustand 日志中间件」—— 要求:
- 拦截 store 的 set 操作,打印「更新前状态」和「更新后状态」;
- 支持异步 set 操作(比如异步修改状态);
- 无需依赖 Zustand 源码,用伪代码模拟核心逻辑即可。
// 模拟Zustand的create函数(带中间件支持)
const create = (initializer) => {
let state;
// 中间件包装后的set方法
const setState = (updater) => {
// 处理函数式更新(如 (s) => ({ count: s.count + 1 }))
const newState = typeof updater === 'function' ? updater(state) : updater;
state = { ...state, ...newState }; // 合并新状态
};
// 初始化store(执行用户传入的initializer)
state = initializer(setState, () => state);
return {
getState: () => ({ ...state }), // 返回状态副本,避免外部修改
setState,
};
};
// 使用中间件创建store
const initializer = (set, get) => ({
count: 0,
// 同步方法
increment: () => set((s) => ({ count: s.count + 1 })),
// 异步方法
asyncIncrement: async () => {
await new Promise((resolve) => setTimeout(resolve, 1000));
set((s) => ({ count: s.count + 1 }));
},
});
const useCounterStore = create(initializer);
// 实现日志中间件(洋葱模型思路) 调用 setState 前,打印「更新前状态:xxx」,调用 setState 后,打印「更新后状态:xxx」;
// const logMiddleware = 实现
// const useCounterStore = create(logMiddleware(initializer));
// 测试
console.log('初始状态:', useCounterStore.getState()); // 初始状态:{ count: 0 }
useCounterStore.increment(); // 触发同步更新
useCounterStore.asyncIncrement(); // 触发异步更新
- 除了 Zustand/Koa,你还知道哪些前端框架 / 库用到了洋葱模型?它在这些场景中解决了什么问题?
- 洋葱模型和「责任链模式」有什么区别?请举例说明(比如两者在处理请求时的不同逻辑)。
- 假设你正在开发一个接口请求工具,需要通过中间件实现「请求拦截(加 Token)」「响应拦截(统一处理错误)」「日志记录(打印请求耗时)」,请用洋葱模型设计这三个中间件的执行顺序,并简要说明理由。
下面是使用的逻辑,请开发 createRequestEnhancer
// 创建实例
const request = createRequestEnhancer();
// 添加日志中间件(前置+后置逻辑)
request.use(async (ctx, next) => {
console.log('日志:请求开始,URL=', ctx.url);
const start = Date.now();
await next(); // 执行后续中间件+核心请求
console.log('日志:请求结束,耗时=', Date.now() - start, 'ms');
});
// 添加请求拦截中间件(前置逻辑)
request.use(async (ctx, next) => {
console.log('请求拦截:添加Token');
ctx.options.headers = {
...ctx.options.headers,
Authorization: 'Bearer 123456',
};
await next();
});
// 添加响应拦截中间件(后置逻辑)
request.use(async (ctx, next) => {
await next(); // 先执行核心请求
console.log('响应拦截:格式化数据');
ctx.response = { code: 200, data: ctx.response }; // 包装响应
});
// 发送请求(测试洋葱模型)
request
.fetch('https://jsonplaceholder.typicode.com/todos/1')
.then((res) => console.log('最终结果:', res))
.catch((err) => console.log('错误:', err));
// 执行的时候
// 日志:请求开始,URL= https://jsonplaceholder.typicode.com/todos/1
// 请求拦截:添加Token
// (核心fetch请求)
// 响应拦截:格式化数据
// 日志:请求结束,耗时= 120 ms
// 最终结果: { code: 200, data: { userId: 1, id: 1, title: '...', completed: false } }
- 如果中间件数组很长(比如 100 个),洋葱模型的递归实现会导致栈溢出吗?如果会,如何优化执行器的实现(非递归方式)?
答案
- 请用一句话概括「洋葱模型」的核心执行逻辑,并用通俗的例子解释它的应用场景?
参考:洋葱模型的关键是 “逐层进入、逐层返回” 的双向流程,请求先逐层穿过外层中间件到达核心逻辑,再反向逐层穿过外层中间件返回(“进 - 核心 - 出” 的双向流程)。通俗例子:比如 Koa 处理 HTTP 请求时,先通过日志中间件记录请求开始(进),再通过权限中间件校验身份(进),执行核心的接口处理逻辑后,再通过权限中间件记录校验结果(出),最后通过日志中间件记录请求结束(出),全程不修改核心接口逻辑。
- 洋葱模型中,next() 函数的核心作用是什么?如果某个中间件里不调用 next(),会发生什么?
- next() 的核心作用:触发下一个中间件或核心逻辑的执行,是串联洋葱模型 “逐层进入” 的关键,同时保证执行完后续逻辑后能回到当前中间件的 await next() 之后(实现 “逐层返回”)。
- 若某个中间件不调用 next():后续所有中间件和核心逻辑都会被阻断(相当于 “拦截”),当前中间件 next() 之后的代码也不会执行(因为没有后续逻辑触发返回)。
-
请写出最终的控制台输出顺序(包括耗时相关日志)?
A 进 -> B 进 -> (等待 1s) -> C 进 -> 核心逻辑 -> C 出 -> B 出 -> A 出
-
执行完后,ctx.msg 的值是什么?
ABC 核心
-
如果把中间件 B 的 await next() 改成 next()(去掉 await),输出顺序会发生什么变化?为什么?
中间件 B 的代码里有 await new Promise(resolve => setTimeout(resolve, 1000))(1s 异步延迟),如果把 await next() 改成 next(),执行顺序会变成:A 进 -> B 进 (触发 next()但不等待,直接执行 console.log('B 出'))-> B 出 -> A 出(1s 后) -> C 进 -> 核心逻辑 -> C 出。因为 next 是 async 函数,所以会返回一个 Promise,进入了微任务队列。
// 中间件B的代码
async (ctx, next) => {
console.log('B 进');
await new Promise((resolve) => setTimeout(resolve, 1000)); // 等待1s(宏任务完成)
// 调用 next(),但不 await
next(); // next()是 async 函数,返回 Promise,进入微任务队列
console.log('B 出'); // 主线程代码,直接执行
};
// 中间件 C 的代码(同步)
async (ctx, next) => {
console.log('C 进'); // 微任务:需等主线程执行完才会触发
await next(); // 同步执行核心逻辑
console.log('C 出');
};
-
请基于洋葱模型,实现一个简化版的「Zustand 日志中间件」—— 要求:
- 拦截 store 的 set 操作,打印「更新前状态」和「更新后状态」;
- 支持异步 set 操作(比如异步修改状态);
- 无需依赖 Zustand 源码,用伪代码模拟核心逻辑即可。
create(logMiddleware(initializer))这个是基于create(initializer))的增强版,也就是 logMiddleware(initializer)的返回值是类似initializer,initializer 这个函数入参是 set 和 get,返回值不知道是是啥,但通过 initializer(set, get) 可以拿到返回值。
本次写的中间件,是拦截 set 操作,也就是拿到 set 方法,然后包装成一个增强后的 set 方法,这个增强后的 set 方法,会在调用原始 set 方法之前,打印「更新前状态」,在调用原始 set 方法之后,打印「更新后状态」。再把这个增强后的 set 方法传给原始 initializer 函数,执行 initializer 函数,就是返回值了。
// 实现日志中间件(洋葱模型思路)
const logMiddleware = (initializer) => {
return (set, get) => {
// 包装原始set方法,添加日志逻辑
const enhancedSet = async (updater) => {
// 1. 进入阶段:打印更新前状态
console.log('更新前状态:', get());
// 处理异步updater(比如传入的是async函数)
const newState =
typeof updater === 'function'
? await updater(get()) // 等待异步函数执行完
: updater;
// 2. 执行核心逻辑:调用原始set方法更新状态
set(newState);
// 3. 离开阶段:打印更新后状态
console.log('更新后状态:', get());
};
// 将增强后的set传给原始配置(洋葱模型的“next”逻辑)
return initializer(enhancedSet, get);
};
};
- create(initializer) 的本质:initializer 是一个函数,接收 set/get,返回初始状态对象(比如 { count: 0, increment: ... }),create 执行它后会把返回值作为初始 state。
- 中间件的作用链:logMiddleware(initializer) → 返回一个新的初始化函数(我们叫它 enhancedInitializer);这个 enhancedInitializer 会接收 create 传入的原始 set/get,然后包装 set(比如加日志),再把增强后的 set 传给原始 initializer 执行;最终 create 拿到的是 “增强版 set 执行后返回的状态”,实现对 set 操作的拦截。
- 核心目标:不修改原始 initializer 的逻辑,只通过包装 set/get 实现功能增强 —— 这正是中间件 “开放 - 封闭原则” 的体现。
- 可以说 “中间件是对 initializer 执行逻辑的增强”,但更准确的是:中间件通过包装 set/get 方法,间接增强 initializer 的执行效果——initializer 本身的业务逻辑不变,但它调用的 set 被增强了,最终实现功能扩展。
- 除了 Zustand/Koa,你还知道哪些前端框架 / 库用到了洋葱模型?它在这些场景中解决了什么问题?
还有 express 的中间件,线性执行模型,经典的中间件模式
Express 会把所有 app.use()/app.get() 注册的中间件 / 路由处理函数,按注册顺序存入一个数组,请求到来时依次执行,直到遇到 res.end() 或 next()
// 模拟 Express 的中间件容器
const middlewareStack = [];
// 模拟 app.use():注册中间件
function use(middleware) {
middlewareStack.push(middleware);
}
// 模拟请求处理:依次执行中间件
function handleRequest(req, res) {
let index = 0; // 记录当前执行的中间件下标
// 定义 next 函数:执行下一个中间件
function next() {
if (index < middlewareStack.length) {
const currentMiddleware = middlewareStack[index];
index++;
currentMiddleware(req, res, next); // 传入 next,让中间件手动调用
}
}
next(); // 启动执行第一个中间件
}
// 使用的时候
// 注册中间件(按顺序)
use((req, res, next) => {
console.log('中间件1:记录请求日志');
next(); // 调用 next 执行下一个
});
use((req, res, next) => {
console.log('中间件2:校验用户身份');
req.user = { id: 1 };
next(); // 调用 next 执行下一个
});
use((req, res, next) => {
console.log('中间件3:处理路由逻辑');
res.end(`Hello ${req.user.id}`); // 没有 next,流程终止
});
// 模拟请求
handleRequest({}, { end: (msg) => console.log('响应:', msg) });
// 输出:
// 中间件1:记录请求日志
// 中间件2:校验用户身份
// 中间件3:处理路由逻辑
// 响应:Hello 1
- 洋葱模型和「责任链模式」有什么区别?请举例说明(比如两者在处理请求时的不同逻辑)。
责任链像工厂的流水线,每个工位(中间件)都做一部分工作,最终产出成品(处理完请求),工位之间是 “接力” 关系。其核心是把多个独立的 “处理环节” 串成一条线,每个环节都是流程的一部分(没有明确的 “主逻辑”)—— 请求从第一个环节流到最后一个环节,每个环节都可能成为 “终点”(比如拦截请求、处理业务)。审批系统的 “员工申请 → 组长审批 → 经理审批 → 财务打款”,每个环节都是流程的必要步骤,没有 “辅助” 之说。
洋葱模型像给核心零件(主逻辑)包保护膜,内层是核心零件,外层的膜(中间件)负责防护、装饰,膜不改变零件本身,只增强功能。洋葱模型的核心是围绕一个明确的 “主逻辑”,用多层辅助逻辑做前后增强—— 主逻辑(如 Koa 的路由处理、Zustand 的 set 操作)是核心,其他中间件都是 “配角”,只负责前置 / 后置的辅助工作(日志、统计、拦截等)。
- 假设你正在开发一个接口请求工具,需要通过中间件实现「请求拦截(加 Token)」「响应拦截(统一处理错误)」「日志记录(打印请求耗时)」,请用洋葱模型设计这三个中间件的执行顺序,并简要说明理由。
function createRequestEnhancer() {
const middlewares = [];
const use = (middlewareFn) => {
middlewares.push(middlewareFn);
};
const executeOnion = async (middlewares, ctx, coreFn) => {
let index = 0;
async function next() {
if (index < middlewares.length) {
// 这里是 < 不是 <=,避免越界
const curMiddleware = middlewares[index];
index++; // 先index++,再执行中间件(否则会重复执行第一个)
await curMiddleware(ctx, next);
} else {
// 核心逻辑:执行fetch并把结果存入ctx
const res = await coreFn(ctx.url, ctx.options);
ctx.response = await res.json(); // 把响应挂载到ctx,供后续中间件使用
}
}
await next();
return ctx.response; // 最终返回响应结果
};
const enhancedFetch = async (url, options = {}) => {
// 加async,支持await
const ctx = { url, options, response: null }; // 初始化response
return await executeOnion(middlewares, ctx, fetch); // 等待executeOnion完成
};
return {
use,
fetch: enhancedFetch,
};
}
10.如果中间件数组很长(比如 100 个),洋葱模型的递归实现会导致栈溢出吗?如果会,如何优化执行器的实现(非递归方式)?
洋葱模型的递归实现(通过 next() 递归调用中间件)在中间件数量极多(比如 1000+)时,会导致栈溢出—— 因为 JavaScript 的调用栈深度有限(通常几千层),每递归一次就会向调用栈压入一层函数,超过阈值就会抛出 Maximum call stack size exceeded 错误。
但如果只是 100 个中间件,递归通常不会溢出(现代浏览器调用栈深度约 10000 层);但从健壮性角度,非递归实现更可靠。
优化方案:用迭代替代递归实现洋葱执行器
核心思路:把中间件执行逻辑从 “递归调用栈” 改为 “迭代 Promise 链”,通过循环依次执行中间件,利用 Promise 的异步特性避免栈溢出。
/**
* 非递归洋葱执行器
* @param {Array} middlewares - 中间件数组(每个中间件是 (ctx, next) => {})
* @param {Object} ctx - 上下文对象
* @param {Function} coreFn - 核心逻辑函数
* @returns {Promise} - 执行结果
*/
function onionExecutor(middlewares, ctx, coreFn) {
// 1. 把核心逻辑包装成最后一个“中间件”
let nextMiddleware = async (ctx) => {
await coreFn(ctx);
return ctx;
};
// 2. 从后往前遍历中间件数组,逐个包装成嵌套链
// 比如中间件是 [A,B,C],遍历顺序是 C → B → A
for (let i = middlewares.length - 1; i >= 0; i--) {
const currentMiddleware = middlewares[i];
// 保存当前的nextMiddleware(下一个要执行的函数)
const prevNext = nextMiddleware;
// 重新定义nextMiddleware:当前中间件包裹prevNext
nextMiddleware = async (ctx) => {
await currentMiddleware(ctx, async () => await prevNext(ctx));
return ctx;
};
}
// 3. 执行最终构建好的链
return nextMiddleware(ctx);
// 从最后一个中间件开始,反向构建Promise链
//也可以用 reduceRight 实现
// return middlewares.reduceRight((nextMiddleware, currentMiddleware) => {
// // currentMiddleware需要调用nextMiddleware(即下一个中间件)
// return async (ctx) => {
// await currentMiddleware(ctx, async () => await nextMiddleware(ctx));
// return ctx;
// };
// }, finalMiddleware)(ctx);
}
用具体例子拆解构建过程(中间件 [A,B,C]):
假设中间件数组是 [A,B,C],核心逻辑是 coreFn,我们从后往前遍历
第一步:处理最后一个中间件 C
运行;
// 初始 nextMiddleware 是 coreFn
prevNext = coreFn;
// 把 C 和 coreFn 包装成新的 nextMiddleware
nextMiddleware = (ctx) => C(ctx, () => coreFn(ctx));
第二步:处理中间件 B
// 保存当前的 nextMiddleware(即 C+coreFn)
prevNext = (ctx) => C(ctx, () => coreFn(ctx));
// 把 B 和 C+coreFn 包装成新的 nextMiddleware
nextMiddleware = (ctx) => B(ctx, () => C(ctx, () => coreFn(ctx)));
第三步:处理第一个中间件 A
// 保存当前的 nextMiddleware(即 B+C+coreFn)
prevNext = (ctx) => B(ctx, () => C(ctx, () => coreFn(ctx)));
// 把 A 和 B+C+coreFn 包装成新的 nextMiddleware
nextMiddleware = (ctx) => A(ctx, () => B(ctx, () => C(ctx, () => coreFn(ctx))));
最终得到的 nextMiddleware 就是:A(ctx, () => B(ctx, () => C(ctx, () => coreFn(ctx))))
完整使用示例
// 模拟1000个中间件(测试栈溢出)
const middlewares = Array.from({ length: 1000 }, (_, index) => {
return async (ctx, next) => {
ctx.count += 1;
await next(); // 这里的next是迭代构建的Promise链,非递归
ctx.count += 1;
};
});
// 核心逻辑:修改状态
const coreFn = async (ctx) => {
ctx.value = '核心逻辑执行';
};
// 执行器调用
const ctx = { count: 0, value: '' };
onionExecutor(middlewares, ctx, coreFn).then((res) => {
console.log(res.count); // 2000(每个中间件执行两次count++)
console.log(res.value); // 核心逻辑执行
});
原理说明
- 反向 reduce 构建链:从最后一个中间件开始,用 reduceRight 把中间件嵌套成一个 Promise 链 —— 每个中间件的 next() 指向 “下一个中间件的执行函数”,而非递归调用自身。
- 异步解耦:利用 Promise 的异步特性,每次执行 next() 都是一个新的 Promise,不会压入调用栈,而是进入微任务队列,彻底避免栈溢出。
- 核心逻辑收尾:把核心函数作为最后一个中间件,确保所有中间件执行完后才执行核心逻辑。