普通视图

发现新文章,点击刷新页面。
昨天 — 2025年6月27日首页

4、Redux 核心架构源码解读(下)——中间件机制的实现与函数式魅力

作者 付泽宸
2025年6月27日 10:55

✍️ 模拟 Redux 创始人 Dan Abramov 内心设计思维

🎯 聚焦:applyMiddleware 如何劫持 dispatch

🔍 关键词:中间件链、函数组合、dispatch 增强、洋葱模型(Onion Model)


🍩 我们为什么设计 middleware?

Redux 的原始设计只处理同步状态流,但在真实业务中我们还需要:

  • 发起异步请求(如 fetch
  • 记录日志、性能分析
  • 上报埋点、安全审计
  • 报错捕获
  • 权限拦截 / 重定向跳转

你不希望把这些“副作用逻辑”塞到 reducer 里,于是我们设计了一个**“dispatch 增强系统”**:

中间件(Middleware)本质上是一个“可插拔的 dispatch 劫持器”。


🧪 看例子:如何写一个日志中间件?

const logger = store => next => action => {
  console.log('dispatching', action)
  const result = next(action)
  console.log('next state', store.getState())
  return result
}

🔍 结构拆解:

middleware = store => next => action => {}
  • store:当前 Redux store 实例
  • next:下一个中间件或原始 dispatch
  • action:你要分发的 action

这个结构,其实就是洋葱模型函数链式封装


🧰 applyMiddleware 的源码实现(简化版)

function applyMiddleware(...middlewares) {
  return (createStore) => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)
    let dispatch = store.dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: (...args) => dispatch(...args)
    }

    const chain = middlewares.map(mw => mw(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return { ...store, dispatch }
  }
}

🧠 深度解读每一步

🧩 middlewareAPI

const middlewareAPI = {
  getState: store.getState,
  dispatch: (...args) => dispatch(...args)
}

👉 把最基本的能力(状态获取、触发 action)暴露出去给中间件用,但通过闭包劫持 dispatch,确保是增强后的 dispatch


🧰 compose:中间件链的秘密武器

function compose(...funcs) {
  return funcs.reduceRight((a, b) => (...args) => b(a(...args)))
}

这个函数做了什么?

compose(fn1, fn2, fn3)(dispatch)
// 相当于 fn1(fn2(fn3(dispatch)))

每个中间件包裹住下一个中间件,最终将 action 传给原始 dispatch。这就是**“洋葱模型”**。


🧪 举个更形象的例子:

我们注册了如下中间件:

applyMiddleware(logger1, logger2, thunk)

会得到如下执行链:

dispatch = logger1(logger2(thunk(dispatch)))

当你调用 store.dispatch(action)

  1. logger1 先执行,记录日志;
  2. 再交给 logger2 做拦截;
  3. 最后进入 thunk 判断是否为函数;
  4. 最终调用原始 reducer。

🔄 我们这样设计的原因只有一个:高度可组合性

中间件是 Redux 的“插件系统”,但我们坚持这几点:

原则 解释
无依赖注入 middleware 只依赖 getStatedispatch
无状态 中间件函数本身是无状态函数,便于复用、测试
只包一次 dispatch 所有中间件最终作用于 dispatch,逻辑单一清晰

🔥 自定义中间件场景推荐:

场景 建议实现方式
打印日志 loggerMiddleware
异步请求 redux-thunk / redux-saga
权限验证 authMiddleware
请求去重 requestCacheMiddleware
报错捕获上报 errorCaptureMiddleware
多语言替换 i18nMiddleware

🪢 总结:Redux 中间件的真正威力

中间件是 Redux 最被模仿的部分(如 Express/Koa 的中间件链),其威力来源于三点:

  1. 函数式组合:易读、可测试、抽象度高;
  2. 扩展性强:可插拔、无副作用污染;
  3. 执行链可控:你可以控制 action 的完整生命周期。

它将“架构的中间环节”变成了“第一等公民”。


⏭️ 下一篇预告

我们即将踏入 UI 层的 Redux 世界,下一篇将深入剖析:React-Redux 是如何用 Context + 高阶组件连接 UI 与 Redux 的?

你会看到 connect(mapStateToProps) 背后究竟做了哪些性能优化与底层 magic。

3、Redux 核心架构源码解读(上)——createStore 是如何运作的?

作者 付泽宸
2025年6月27日 10:54

✍️ 视角模拟:Redux 作者 Dan Abramov 的内心独白

🧠 主题:createStore 的设计底层逻辑

🔍 关键词:闭包、订阅发布、事件模型、状态快照、架构最小内核


🧬 我们为什么只用 40 行代码实现 Redux 内核?

当我们(Dan Abramov 和 Andrew Clark)决定创造 Redux 时,有一个目标非常清晰:

“最小可用的状态容器,只关心同步数据流的状态变更。”

我们拒绝一切魔法、拒绝隐藏副作用,只提供:

  • 状态读取(getState)
  • 状态更新(dispatch)
  • 订阅监听(subscribe)

🔧 先看源码:Redux 内核是如何构成的?

下面是 Redux 最早期版本 createStore 的核心实现(约 40 行):

function createStore(reducer, preloadedState) {
  let currentState = preloadedState
  let currentListeners = []

  function getState() {
    return currentState
  }

  function subscribe(listener) {
    currentListeners.push(listener)
    return () => {
      const index = currentListeners.indexOf(listener)
      currentListeners.splice(index, 1)
    }
  }

  function dispatch(action) {
    currentState = reducer(currentState, action)
    currentListeners.forEach(listener => listener())
  }

  // 初始化 state
  dispatch({ type: '@@redux/INIT' })

  return { getState, dispatch, subscribe }
}

🧠 深度剖析每一个函数的设计思维

1️⃣ getState():只读数据,不准改

function getState() {
  return currentState
}

设计思路:

  • 绝不提供 setState
  • 所有外部只能 dispatch
  • 核心思想:不让“谁都能修改状态”成为潜在地雷。

2️⃣ subscribe(listener):注册监听器

function subscribe(listener) {
  currentListeners.push(listener)
  return () => { /* 取消订阅逻辑 */ }
}

架构背后:

  • 这是一个事件模型;
  • 我们采用 观察者模式,但不依赖外部事件库;
  • Redux 自带的订阅,是为UI 层(比如 React-Redux)服务的;
  • 返回取消函数(unsubscribe),是参考 DOM 的 addEventListener

3️⃣ dispatch(action):唯一的状态变更入口

function dispatch(action) {
  currentState = reducer(currentState, action)
  currentListeners.forEach(listener => listener())
}

为什么强制通过 dispatch

因为我们想构建一个“状态快照流水线”:

  • 每次调用 dispatch,都有明确的前置状态与后置状态;
  • 如果你把这些状态保存下来,就可以做时间旅行(Time Travel Debugging);
  • 这也是 Redux-DevTools 的实现原理。

4️⃣ reducer 是什么角色?

currentState = reducer(currentState, action)

这是整个 Redux 的“大脑”:

  • 接收 state + action;
  • 返回新的 state;
  • 必须是纯函数;
  • 它的本质是:状态转移矩阵(State Transition Graph)

这就是为什么我们不允许在 reducer 里搞异步、不允许副作用——

❗ Redux 并不是“只为你服务的状态工具”,它也是一个“调试友好系统”。 所以我们用“暴力纯函数”强迫你把状态变更流程写清楚。


🧪 示例:还原 Redux 的最简内核并运行

const reducer = (state = { count: 0 }, action) => {
  switch (action.type) {
    case 'INC': return { count: state.count + 1 }
    default: return state
  }
}

const store = createStore(reducer)

store.subscribe(() => {
  console.log('状态变化:', store.getState())
})

store.dispatch({ type: 'INC' }) // { count: 1 }
store.dispatch({ type: 'INC' }) // { count: 2 }

🧩 为什么我们不一开始就支持异步?

这是 Redux 最常被诟病的点。但我想告诉你:

Redux 从不处理异步。它是同步状态的架构。异步属于你的架构边界,属于 middleware。

正因如此,我们才设计了 applyMiddleware(下一篇会讲)——让你用 redux-thunk、redux-saga 等方式做副作用管理。


🪢 总结:“极简”才是 Redux 的杀手锏

功能 背后动机
getState 避免重复请求与全局依赖,保持透明性
dispatch 状态变更唯一入口,保证一致性与调试能力
subscribe UI 响应状态变化的桥梁
reducer 可组合、可测试的状态管理核心
init dispatch 初始化状态结构(类似默认构造器)

Redux 不靠「黑魔法」,靠的是最基础、最纯粹的闭包与事件模型。这就是它如此稳定、长寿、易维护的根本原因。

2、三句话撑起 Redux 帝国——架构设计的第一性原理

作者 付泽宸
2025年6月27日 10:53

✍️ 模拟 Dan Abramov 思路撰写

🎯 主题:Redux 三大原则并非限制,而是可控性的构建工具

🧠 关键词:单一状态树、只读状态、纯函数 reducer、前端状态建模


📜 Redux 只立了三条法则,但每一条都是“架构杀器”

Redux 的三条核心原则,很多人第一眼会觉得像“开发规范”:

  1. 单一数据源
  2. 状态只读
  3. 状态变更只能通过纯函数 reducer

但你错了——

这三条不是代码规范,而是前端状态世界的物理定律。 它们是为了让你在高复杂系统中保持可控可测可审计可扩展

我设计 Redux 时的第一个问题是:

“如果一个状态出了错,我怎么定位、调试、还原?”

这三条法则,就是我在“如何让状态变更变得可观测”这道题上的最终答案。


🧱 法则一:单一状态树(Single Source of Truth)

const state = {
  user: {...},
  posts: [...],
  ui: {...}
}

为什么不是多个 store?为什么不是组件自己持有状态?

因为:

  1. 多 store 会导致状态分散,难以跨模块联动(如一个 user 状态改了,要同时更新多个地方);
  2. 多个 source of truth 会让状态无法被还原、同步、持久化、回放;
  3. 全局状态不可组合,会导致“状态碎片化”。

单一状态树让我们获得了:

  • 🔍 快照能力(任何时刻 state 可打印、可序列化)
  • ⏪ 状态回放(DevTools 回溯)
  • 🪝 状态监听(React-Redux 的订阅更新)
  • 🔄 服务端同步(hydrate)

“可观察性,是工程能力的前提。”


🚫 法则二:状态是只读的(Read-Only)

你可能会问:

“我直接改 store.state.user.name = 'Mark' 有什么问题?”

问题不是现在看得见,而是以后你调不出来。

我们把所有状态修改路径收敛dispatch(action)

store.dispatch({ type: 'UPDATE_USER', payload: { name: 'Mark' } })

带来了哪些能力?

  • 每次修改都有日志(如 DevTools TimeLine);
  • 状态不会被“偷偷改掉”;
  • 中间件可以拦截变更前后;
  • 你可以记录/重放所有用户操作。

这不是“麻烦”,而是换取长期稳定性的代价

我们牺牲了自由,但换来了:

状态修改的 唯一性、显式性、可拦截性、可验证性


🧪 法则三:状态变更只能由纯函数 reducer 执行

function reducer(state, action) {
  if (action.type === 'ADD') {
    return { ...state, count: state.count + 1 }
  }
  return state
}

什么是纯函数?

  • 不修改外部变量;
  • 相同输入 ➜ 相同输出;
  • 不产生副作用(如请求、日志、跳转等)。

为什么强调纯?因为我们想让你“可测”。

test('reducer works', () => {
  expect(reducer({ count: 1 }, { type: 'ADD' })).toEqual({ count: 2 })
})

如果你的 reducer 里面写了 API 请求或随机数,那你就测试不了了。

“可预测性 + 可测试性 = 可维护性。”


🔬 Redux = 状态行为的数学建模系统

这三大原则合在一起,让我们可以将 Redux 看作一个状态行为的有向图系统:

State₀ --(action₁)--> State₁ --(action₂)--> State₂ ...

你拥有:

  • 明确的状态变更路径;
  • 明确的变更记录(action 日志);
  • 明确的转移函数(reducer);
  • 明确的订阅机制(connect/useSelector);

这种设计可以类比为:

  • 📦 数据库的 WAL(Write Ahead Log)机制
  • 🛠️ Git 的 commit 流
  • 🎞️ 时间机器的时间轴系统

你拥有的是一份“前端状态的真实录像带”。


🔁 “限制”换来的是工程级别的自由

很多人会说 Redux 繁琐、冗长、写得累。

我想问:

你愿意用 setState 到处改状态,在线上踩无数坑,还是提前架设防线?

当你业务变得复杂:

  • 多人协作、多人并发
  • 状态之间强关联
  • 调试成本高
  • 跨端同步、历史还原

你会发现 Redux 是为这些场景生的。


✅ Redux 工程实践建议

项目场景 是否推荐使用 Redux
简单组件内部状态 ❌ 推荐 useState / useReducer
中型业务系统,有多个模块状态 ✅ 推荐使用 Redux + Redux Toolkit
多端共享状态、状态需持久化、需调试 ✅ 强烈推荐使用 Redux + DevTools
组件树层级深、状态传递层级深 ✅ 推荐 Redux 或 Context + Hooks

🔚 总结:Redux 的三大原则,不是限制你,是保护你

Redux 的三大法则,是我们从架构维度写给工程师的一封安全保障书:

我们不是为了让你代码更优雅,而是让你在 1 年、3 年、5 年之后还能维护这套系统。

它们构建的不是“代码风格”,而是一个:

  • ✅ 可追踪(Traceable)
  • ✅ 可预测(Predictable)
  • ✅ 可还原(Reproducible)
  • ✅ 可协作(Collaborative)

的状态模型。


⏭️ 下一篇预告

你听我讲了这么多哲学,是时候打开 Redux 的大脑了:

下一篇,我们进入 Redux 的核心函数:createStore()。 我将逐行带你解析 Redux 是如何实现状态容器、监听机制、以及 dispatch 流程的。

1、为什么 Redux 值得存在?一个状态架构的觉醒

作者 付泽宸
2025年6月27日 10:53

✍️ 模拟 Dan Abramov 思路与设计哲学

🎯 主线:不是为了解决“多组件通信”——而是为了解决状态可控性

🧠 关键词:状态演化、架构异味、不可预测性、调试性、时间旅行


🧨 状态是前端复杂性的第一根导火索

我们不是一开始就要写 Redux 的。 我(Dan Abramov)最早只是遇到一个问题:

在一个中型 React 项目里,我根本不知道哪个组件在什么时候修改了状态。

setState 非常方便,但项目开始变得“不可预测”:

  • 状态到处都是,每个组件都在维护自己的小世界;
  • 多人协作时,彼此 props 改一改就全乱;
  • Bug 不可复现,调试靠猜。

我们不想再靠“命名规范”和“PR 评论”去管状态。我们要架构层面的强约束


🔀 Flux 是启发,但 Redux 不止是 Flux

当时 Facebook 提出了 Flux 架构,给了我们一个启发:

View → Action → Dispatcher → Store → View

单向数据流,没错,这是关键思想。

但 Flux 有几个问题:

  • Dispatcher 是黑盒;
  • 没有标准工具链;
  • 代码太啰嗦,写一个操作要 5 个文件。

所以我想:有没有一种方式,把 Flux 的数据可控性留下,同时引入函数式编程的纯洁性与组合性

答案就是 Redux。


🔧 Redux 最初版本只有 99 行

是的,第一版 Redux 源码不到 100 行。我们只做了几件事:

  • 把状态放在唯一的 state 树中;
  • 所有修改必须经过 dispatch(action)
  • 所有变更逻辑写在 reducer(state, action) 这个纯函数里。

换句话说:

我们不是设计了一个“状态管理库”,我们设计了一个“状态更新流程调度系统”。


🧬 Redux 核心哲学:可预测性是第一性原理

为什么状态要不可变? 为什么要纯函数? 为什么不让你直接改 state?

不是因为“代码规范”,而是因为这样你才能得到以下能力:

能力 原理支撑
时间旅行调试 状态快照是不可变的、可还原的
状态还原与回滚 有历史记录链,每次 dispatch 都明确
审计与回放 所有 action 都是结构化事件
多端同步 状态结构可以序列化 + 重构建
自动化测试 reducer 函数是纯的,不依赖外部上下文

换句话说:

Redux 就是“把状态管理模型,变成了数学建模”。


🧱 我们只保留了 3 条规则

  1. 单一数据源 整个应用的状态集中存储在一棵对象树中。可以做持久化、调试、快照、同步等一切。

  2. 状态是只读的 唯一修改方式是 dispatch(action),并通过 reducer 控制行为。

  3. 变更逻辑必须是纯函数 同样的输入一定得到同样的输出。所有变更路径显性,消除副作用污染。

这三条不是“语法规范”,是我们为状态架构定义的 操作系统级别的权限模型


🧪 小例子:Redux 的模型其实像数据库日志系统

dispatch({ type: 'INCREMENT' }) // 相当于写入一条操作日志
reducer(state, action)         // 相当于执行一条事务
getState()                     // 相当于查询当前快照

Redux 像一个“前端 mini 数据库”,具备:

  • 写入事务日志(action)
  • 只读快照(state)
  • 流水线 replay(DevTools 回放)
  • 插件扩展能力(middleware)

🤝 它为多人协作带来了什么?

在多人协作的项目里,Redux 能解决的痛点是:

  • 团队对“数据流动”的认知统一;
  • reducer 可以被团队模块维护;
  • 中间件实现权限、埋点、打点统一;
  • 更容易做自动化测试 + 日志上报。

💬 最常见的问题:Redux 太重?

“你写个按钮还得 dispatch?reducer?action?”

这个问题就像问:“你修一扇门,干嘛要画房屋设计图?”

Redux 从来不是为“按钮计数器”而生,它为的是:

  • 多模块协作;
  • 多人并行开发;
  • 状态的长期维护、升级、调试。

所以我们推出了 redux-toolkit,来简化语法,但不简化哲学


🔚 总结:Redux 的目标不是方便,而是可控

我们从来不是要让你“少写几行代码”。

我们要让你:

  • 明确状态从哪里来,到哪里去;
  • 拥有状态调试和回溯的能力;
  • 构建一个可以长期维护的应用架构。

如果你只追求“快速出活”,那不一定需要 Redux。 但如果你在构建一个工程化体系下的前端应用平台,Redux 仍然是最纯粹、最透明、最强大的状态管理工具。


⏭️ 下一篇预告

Redux 的哲学我们讲清了,接下来,我们要把这颗“哲学内核”逐行拆开。 在下一篇,我们将进入 createStore(),亲手实现 Redux 的最小状态内核,并解读它的设计精髓。

❌
❌