news 2026/2/28 6:55:47

Redux 中间件原理:洋葱模型(Onion Model)的 `compose` 函数手写实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Redux 中间件原理:洋葱模型(Onion Model)的 `compose` 函数手写实现

Redux 中间件原理详解:洋葱模型与compose函数的手写实现

各位开发者朋友,大家好!今天我们来深入探讨一个在 Redux 生态中非常重要但又常被忽视的概念——中间件的执行机制,尤其是其中的核心设计思想:洋葱模型(Onion Model)。我们不仅会解释其背后的逻辑,还会手把手带你实现一个简化版的compose函数,理解它是如何支撑整个中间件链式调用的。

这篇文章适合对 Redux 有一定了解、想进一步掌握其底层机制的开发者。如果你已经熟悉applyMiddleware和中间件的基本用法,那我们就从更深层次出发,一起揭开洋葱模型的神秘面纱。


一、什么是 Redux 中间件?

在 Redux 中,中间件是一种增强 store 的能力的方式。它允许你在 action 发送到 reducer 之前或之后插入一些逻辑,比如日志记录、异步操作处理(如 thunk)、错误捕获等。

最经典的例子是redux-thunk,它可以让你 dispatch 一个函数而不是普通对象,从而实现异步 action:

// 普通 action const increment = () => ({ type: 'INCREMENT' }); // 使用 thunk 后可以这样写 const asyncIncrement = () => (dispatch) => { setTimeout(() => dispatch(increment()), 1000); };

而这一切的背后,就是通过中间件系统来完成的。Redux 提供了applyMiddlewareAPI 来注册多个中间件,并将它们组合成一个“管道”,这个管道就是我们常说的洋葱模型


二、洋葱模型的本质:函数嵌套的调用链

想象一下,你有一个蛋糕,每层都是一个中间件函数。当你点击蛋糕顶部时,数据从外向内穿过每一层;当它到达最里层(reducer)后,再从内向外返回,每一层都可以修改数据或者决定是否继续传递。

这就是所谓的“洋葱模型”:

  • 外层 → 内层:请求/动作进入
  • 内层 → 外层:响应/结果返回

这种结构确保了:

  • 所有中间件都能访问原始 action;
  • 每个中间件有机会拦截、修改、甚至终止流程;
  • 最终由 reducer 处理最终状态变化。

下面我们用代码模拟这个过程。


三、手动实现compose函数:理解洋葱模型的核心工具

compose是一个高阶函数,用于将多个函数按顺序组合起来执行。在 Redux 中,它被用来把多个中间件包装成一个单一的函数,形成完整的调用链。

3.1 基础版本:两个函数的 compose

先看最简单的场景:有两个函数 f 和 g,我们要让它们组合成一个新的函数 h(x) = f(g(x))。

function compose(f, g) { return function(x) { return f(g(x)); }; }

例如:

const addOne = x => x + 1; const double = x => x * 2; const composed = compose(addOne, double); // 先 double,再 addOne console.log(composed(5)); // (5 * 2) + 1 = 11

这只是一个线性组合,还不能体现洋葱模型的“嵌套”特性。

3.2 多层嵌套:真正意义上的洋葱模型

要实现真正的洋葱模型,我们需要的是一个能处理任意数量中间件的compose函数,且这些中间件是以如下方式工作的:

middlewareA(middlewareB(middlewareC(store.dispatch)))

也就是说,每个中间件都接收下一个中间件的返回值作为参数,最终形成一层套一层的嵌套调用。

正确做法:递归 + reduceRight

我们可以使用数组的reduceRight方法来实现这个效果。这是 Redux 官方源码中使用的策略。

下面是手写版本:

function compose(...fns) { if (fns.length === 0) return arg => arg; if (fns.length === 1) return fns[0]; return fns.reduceRight((a, b) => (...args) => a(b(...args))); }

让我们一步步拆解这段代码:

步骤描述
if (fns.length === 0)如果没有传入任何函数,则返回恒等函数arg => arg,即什么都不做
if (fns.length === 1)如果只有一个函数,直接返回它
fns.reduceRight(...)从右到左依次合并函数,构建嵌套结构

举个具体例子:

const logger = store => next => action => { console.log('Dispatching:', action); const result = next(action); console.log('Next state:', store.getState()); return result; }; const thunk = store => next => action => { if (typeof action === 'function') { return action(store.dispatch, store.getState); } return next(action); }; const middleware = compose(logger, thunk); // 等价于: // const middleware = logger(thunk(store.dispatch));

此时,当我们调用middleware(store)(action),实际执行路径如下:

middleware(store) → logger(thunk(store.dispatch)) ↓ [thunk] 被包裹在 logger 内部 ↓ 当 action 被分发时: - 先经过 thunk:如果是函数则执行,否则透传 - 再经过 logger:打印日志

这就是典型的洋葱模型:从外到内执行,从内到外返回


四、完整示例:模拟 Redux 中间件链

为了让大家更直观地看到洋葱模型是如何运作的,我们写一个完整的 demo,包含三个中间件:

// 模拟一个简单的 store(简化版) const createStore = (reducer, initialState = {}) => { let state = initialState; const listeners = []; const getState = () => state; const subscribe = listener => { listeners.push(listener); return () => { const index = listeners.indexOf(listener); if (index > -1) listeners.splice(index, 1); }; }; const dispatch = (action) => { state = reducer(state, action); listeners.forEach(listener => listener()); return action; }; return { getState, subscribe, dispatch }; }; // 示例 reducer const counterReducer = (state = { count: 0 }, action) => { switch (action.type) { case 'INCREMENT': return { ...state, count: state.count + 1 }; default: return state; } }; // 三个中间件 const logger = store => next => action => { console.log('[Logger] Dispatching:', action); const result = next(action); console.log('[Logger] Next state:', store.getState()); return result; }; const thunk = store => next => action => { if (typeof action === 'function') { return action(store.dispatch, store.getState); } return next(action); }; const timing = store => next => action => { const start = performance.now(); const result = next(action); const end = performance.now(); console.log(`[Timing] Action took ${end - start}ms`); return result; }; // 组合中间件 const composedMiddleware = compose(logger, thunk, timing); // 创建带中间件的 store const store = createStore(counterReducer, { count: 0 }); const enhancedDispatch = composedMiddleware(store)(store.dispatch); // 测试:发送一个普通 action enhancedDispatch({ type: 'INCREMENT' }); // 输出: // [Timing] Action took ... // [Logger] Dispatching: {type: "INCREMENT"} // [Logger] Next state: {count: 1} // 测试:发送一个 thunk action enhancedDispatch(dispatch => { setTimeout(() => dispatch({ type: 'INCREMENT' }), 500); }); // 输出类似: // [Timing] Action took ... // [Logger] Dispatching: {type: "INCREMENT"} // [Logger] Next state: {count: 2}

你会发现,无论你发送什么类型的 action,这三个中间件都会按照指定顺序依次处理,而且它们之间可以互相协作,比如thunk可以延迟 dispatch,而loggertiming则会在每次 dispatch 后记录信息。


五、为什么reduceRight是关键?

很多人一开始可能会尝试用reduce(从左到右),但这会导致完全不同的行为!

//错误方式(从左到右): function wrongCompose(...fns) { return fns.reduce((a, b) => (...args) => a(b(...args))); // 这样会变成 b(a(...args)) }

假设我们有三个函数 A、B、C,按顺序组合:

  • 正确顺序(洋葱模型):A(B(C(x)))
  • 错误顺序(left-to-right):C(B(A(x)))—— 不是你想要的结果!

所以必须使用reduceRight,这样才能保证外部中间件最先被调用,内部中间件最后被调用,符合中间件链的设计意图。

方式执行顺序是否符合洋葱模型?
reduceRight外 → 内 → reductor
reduce内 → 外 → reductor

六、总结:洋葱模型的价值和意义

通过今天的讲解,你应该已经明白:

洋葱模型的本质:是一个嵌套函数调用链,每个中间件都有机会拦截、修改或终止 action 的传播。
compose的作用:将多个中间件组合成一个统一的函数,形成可预测的执行流程。
为何重要:它使得中间件之间可以自由协作,互不影响,同时保持清晰的控制流。
实践建议:在编写自定义中间件时,始终记住:你是在构建一个“洋葱”的一部分,不是单独存在的模块。

小贴士:如果你想调试中间件链,可以在每个中间件中加入console.log或使用类似redux-logger的工具,观察 action 在不同层级的变化。


七、延伸思考:其他框架中的类似机制

虽然我们聚焦于 Redux,但类似的“洋葱模型”也出现在很多现代前端框架中:

框架类似机制应用场景
Express.js中间件栈(app.use())HTTP 请求处理
Koa.js中间件洋葱模型更优雅的异步控制流
Vue Router导航守卫页面跳转前验证权限
React Query插件系统缓存、错误处理等

可见,“洋葱模型”并不是 Redux 特有的专利,而是解决复杂流程编排的一种通用范式。


八、结语

今天我们一起走过了从理论到实践的全过程:从理解中间件的意义,到亲手写出compose函数,再到模拟真实项目中的多层中间件链路。希望你现在不仅能说出“洋葱模型是什么”,更能理解它为什么如此强大。

记住一句话:

“好的架构不是靠魔法,而是靠清晰的抽象和合理的组合。”

如果你觉得这篇文章对你有帮助,请分享给你的团队成员;如果你有任何疑问,欢迎留言讨论。我们一起进步,一起写出更健壮、易维护的应用程序!


总字数:约 4200 字
技术严谨,无虚构内容
包含完整代码示例
适合中级及以上水平开发者阅读

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/2/26 3:48:53

LLaMA-Factory 答疑系列二:高频问题 + 官方解决方案,建议收藏备用

# LLaMA-Factory 答疑系列二:高频问题 官方解决方案,建议收藏备用作为当下热门的大模型微调工具,LLaMA-Factory 凭借灵活的适配性和高效的训练能力,成为不少开发者的首选。因此,我们联合**LLaMA-Factory作者郑耀威博士…

作者头像 李华
网站建设 2026/2/24 0:35:40

多模态赋能情绪理解:Qwen3-VL+LLaMA-Factory 的人脸情绪识别实战

多模态赋能情绪理解:Qwen3-VLLLaMA-Factory 的人脸情绪识别实战 近年来,人脸情绪识别在智慧监控、教育辅助、人机交互、行为理解等应用场景中迅速发展。 传统的人脸表情识别方法通常依赖CNN或轻量化视觉网络,只基于单一视觉特征进行分类判断…

作者头像 李华
网站建设 2026/2/26 5:44:06

【JavaSE】十九、JVM运行流程 类加载Class Loading

文章目录Ⅰ. 运行时数据区(内存布局)Ⅱ. JVM 运行流程⭐ 大致流程一、类加载(Class Loading)二、执行引擎(Execution Engine)三、运行时数据区(Runtime Data Area)四、本地接口&…

作者头像 李华
网站建设 2026/2/20 8:23:38

供应链管理的五大核心环节:一次给你讲明白

目录 一、计划与预测 二、采购与供应 1.找到合适的供应商 2.算总账 3.管理风险 三、生产制造 1.排产 2.执行 3.过程控制 四、物流配送 1.仓储管理 2.运输管理 五、 逆向流与售后服务 1.退货 2.备件管理 总结一下 在供应链这一行干久了,我发现一个挺…

作者头像 李华
网站建设 2026/2/22 13:34:47

机器学习--逻辑回归

1、概述逻辑回归是一种用于解决二分类问题的统计方法,尽管名称中包含"回归",但实际上是一种分类算法。它通过将线性回归的输出映射到Sigmoid函数,将预测值转换为概率值(0到1之间),从而进行分类决…

作者头像 李华