理解洋葱模型

168 阅读8分钟

洋葱模型

洋葱模型(Onion Model)是中间件执行的核心逻辑模式,特点是请求先逐层穿过外层中间件到达核心逻辑,再反向逐层穿过外层中间件返回,形似洋葱的层级结构。每个中间件既可以处理 “进入” 阶段的逻辑(如前置校验),也可以处理 “返回” 阶段的逻辑(如结果处理)。

假设存在两个中间件 A 和 B,以及核心逻辑 C,执行顺序如下:进入 A → 进入 B → 执行 C → 离开 B → 离开 A 整个流程像剥洋葱一样,从外层到内层,再从内层回到外层。

代码实现洋葱模型

executeOnion 函数使用递归和闭包实现了中间件的 “逐层进入、逐层返回” 逻辑,核心是 next() 函数的闭包特性—— 它能记住当前执行到的中间件索引(index),每次调用 next() 就 “推进” 到下一个中间件,直到所有中间件执行完毕后触发核心逻辑。

  • executeOnion 函数是洋葱模型的核心,它接受三个参数:middlewares 中间件数组,core 核心逻辑,ctx 全局上下文对象,然后就是记录index和执行next函数
  • indexexecuteOnion 函数作用域内的变量,next() 作为闭包能持续访问和修改它 —— 每次调用 next()index 就会递增,确保中间件按顺序执行
  • next 函数是每次调用都“消费”一个中间件,每次调用 next()index 就会递增,确保中间件按顺序执行,当 index 达到中间件长度时,执行核心逻辑, 核心逻辑 core 是最后一个中间件,当所有中间件执行完后,执行核心逻辑
  • 每个中间件是一个函数,常用参数是ctxnextctx 是用于在中间件之间、中间件与核心逻辑之间传递数据(如请求参数、状态、结果),避免使用全局变量,保证数据隔离;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() 就像 “打开一扇门进入内层”,只有等内层的所有事情(后续中间件、核心逻辑)全部办完,门才会关上,回到当前中间件继续执行后续代码。这也是为什么中间件的 “离开” 逻辑会按反向顺序执行 —— 内层逻辑必须先完成,外层才能收尾。

考验环节

  1. 请用一句话概括「洋葱模型」的核心执行逻辑,并用通俗的例子解释它的应用场景?
  2. 洋葱模型中,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 += '核心';
};
  1. 请写出最终的控制台输出顺序(包括耗时相关日志)?

  2. 执行完后,ctx.msg 的值是什么?

  3. 如果把中间件 B 的 await next() 改成 next()(去掉 await),输出顺序会发生什么变化?为什么?

  4. 请基于洋葱模型,实现一个简化版的「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(); // 触发异步更新
  1. 除了 Zustand/Koa,你还知道哪些前端框架 / 库用到了洋葱模型?它在这些场景中解决了什么问题?
  2. 洋葱模型和「责任链模式」有什么区别?请举例说明(比如两者在处理请求时的不同逻辑)。
  3. 假设你正在开发一个接口请求工具,需要通过中间件实现「请求拦截(加 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 } }
  1. 如果中间件数组很长(比如 100 个),洋葱模型的递归实现会导致栈溢出吗?如果会,如何优化执行器的实现(非递归方式)?

答案

  1. 请用一句话概括「洋葱模型」的核心执行逻辑,并用通俗的例子解释它的应用场景? 参考:洋葱模型的关键是 “逐层进入、逐层返回” 的双向流程,请求先逐层穿过外层中间件到达核心逻辑,再反向逐层穿过外层中间件返回(“进 - 核心 - 出” 的双向流程)。通俗例子:比如 Koa 处理 HTTP 请求时,先通过日志中间件记录请求开始(进),再通过权限中间件校验身份(进),执行核心的接口处理逻辑后,再通过权限中间件记录校验结果(出),最后通过日志中间件记录请求结束(出),全程不修改核心接口逻辑。
  2. 洋葱模型中,next() 函数的核心作用是什么?如果某个中间件里不调用 next(),会发生什么?
  • next() 的核心作用:触发下一个中间件或核心逻辑的执行,是串联洋葱模型 “逐层进入” 的关键,同时保证执行完后续逻辑后能回到当前中间件的 await next() 之后(实现 “逐层返回”)。
  • 若某个中间件不调用 next():后续所有中间件和核心逻辑都会被阻断(相当于 “拦截”),当前中间件 next() 之后的代码也不会执行(因为没有后续逻辑触发返回)。
  1. 请写出最终的控制台输出顺序(包括耗时相关日志)? A 进 -> B 进 -> (等待 1s) -> C 进 -> 核心逻辑 -> C 出 -> B 出 -> A 出

  2. 执行完后,ctx.msg 的值是什么? ABC 核心

  3. 如果把中间件 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 出');
    };
    
  4. 请基于洋葱模型,实现一个简化版的「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 被增强了,最终实现功能扩展。
  1. 除了 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
  1. 洋葱模型和「责任链模式」有什么区别?请举例说明(比如两者在处理请求时的不同逻辑)。

责任链像工厂的流水线,每个工位(中间件)都做一部分工作,最终产出成品(处理完请求),工位之间是 “接力” 关系。其核心是把多个独立的 “处理环节” 串成一条线,每个环节都是流程的一部分(没有明确的 “主逻辑”)—— 请求从第一个环节流到最后一个环节,每个环节都可能成为 “终点”(比如拦截请求、处理业务)。审批系统的 “员工申请 → 组长审批 → 经理审批 → 财务打款”,每个环节都是流程的必要步骤,没有 “辅助” 之说。

洋葱模型像给核心零件(主逻辑)包保护膜,内层是核心零件,外层的膜(中间件)负责防护、装饰,膜不改变零件本身,只增强功能。洋葱模型的核心是围绕一个明确的 “主逻辑”,用多层辅助逻辑做前后增强—— 主逻辑(如 Koa 的路由处理、Zustand 的 set 操作)是核心,其他中间件都是 “配角”,只负责前置 / 后置的辅助工作(日志、统计、拦截等)。

  1. 假设你正在开发一个接口请求工具,需要通过中间件实现「请求拦截(加 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,不会压入调用栈,而是进入微任务队列,彻底避免栈溢出。
  • 核心逻辑收尾:把核心函数作为最后一个中间件,确保所有中间件执行完后才执行核心逻辑。