Redux 源码精读:1,575 行代码的架构全景
前置知识
- ›JavaScript 基础知识与 ES 模块
- ›有使用 Redux 的实际经验(actions、reducers、store)
Redux 源码精读:1,575 行代码的架构全景
Redux 深刻影响了整整一代 JavaScript 开发者对状态管理的理解方式。然而,在工具集、中间件和开发者工具组成的庞大生态背后,核心库本身只有 17 个源文件,对外导出的运行时值也仅有 9 个。研读这 17 个文件,远不只是一次代码考古之旅 —— 它揭示了一批超越 Redux 本身的设计原则:基于闭包的封装、函数式组合,以及刻意保持极简 API 的力量。
本文将带你梳理这片领域的全貌:逐一介绍每个公开导出,梳理文件与 API 的对应关系,勾勒类型系统的层次结构,解析多格式构建配置,最终揭示支撑 Redux 运转的核心架构思想。
公开 API 一览
所有可以从 redux 导入的内容,都声明在同一个入口文件(barrel file)中。我们先来看看完整内容:
一共 9 个运行时导出,仅此而已。按类别整理如下:
| 导出名 | 类别 | 用途 |
|---|---|---|
createStore |
核心 | 创建 store(已废弃,推荐使用 RTK 的 configureStore) |
legacy_createStore |
核心 | 与上相同,但不会在编辑器中触发废弃警告 |
combineReducers |
组合 | 将多个 slice reducer 合并为单一的根 reducer |
applyMiddleware |
组合 | 将中间件函数组合成 store enhancer |
compose |
工具 | 从右到左的函数组合 |
bindActionCreators |
工具 | 将 action creator 包装为可自动 dispatch 的形式 |
isAction |
类型守卫 | 检查一个值是否为 Redux action |
isPlainObject |
类型守卫 | 检查一个值是否为普通的 {} 对象 |
__DO_NOT_USE__ActionTypes |
逃生舱口 | 内部 action 类型,仅供 DevTools 使用 |
__DO_NOT_USE__ActionTypes 这个命名刻意令人望而却步 —— 它是在向使用者发出信号:这个导出存在的唯一目的是服务于 Redux DevTools 扩展,后者需要知道随机生成的 @@redux/INIT 和 @@redux/REPLACE action 类型字符串。任何应用代码都不应该引用它。
legacy_createStore 这个别名同样值得关注。Redux 团队废弃 createStore 是为了引导用户迁移到 Redux Toolkit,但他们也需要为那些暂时不想迁移的用户提供一条出路,让他们可以避开 IDE 的删除线警告。解决方案就是:新增一个具名导出,指向完全相同的实现。
提示: 阅读陌生代码库时,始终从入口文件(barrel export)开始。它告诉你这个库希望你使用什么,而这通常只是它实际包含内容的一小部分。
目录结构与文件-API 对应关系
Redux 有一个难得一见的特点:源文件与公开 API 函数呈 1:1 对应关系。没有多余的间接层,没有工厂的工厂 —— 每个文件只负责一件事。
| 源文件 | 公开导出 | 行数 |
|---|---|---|
src/createStore.ts |
createStore、legacy_createStore |
~500 |
src/combineReducers.ts |
combineReducers |
~200 |
src/applyMiddleware.ts |
applyMiddleware |
~77 |
src/compose.ts |
compose |
~61 |
src/bindActionCreators.ts |
bindActionCreators |
~83 |
src/types/actions.ts |
类型定义 | ~75 |
src/types/reducers.ts |
类型定义 | ~115 |
src/types/store.ts |
类型定义 | ~233 |
src/types/middleware.ts |
类型定义 | ~31 |
src/utils/actionTypes.ts |
__DO_NOT_USE__ActionTypes |
~17 |
src/utils/isAction.ts |
isAction |
~10 |
src/utils/isPlainObject.ts |
isPlainObject |
~16 |
src/utils/kindOf.ts |
仅内部使用 | ~70 |
src/utils/warning.ts |
仅内部使用 | ~18 |
src/utils/symbol-observable.ts |
仅内部使用 | ~10 |
src/utils/formatProdErrorMessage.ts |
仅内部使用 | ~13 |
graph TD
subgraph "Public API (src/)"
CS[createStore.ts]
CR[combineReducers.ts]
AM[applyMiddleware.ts]
CO[compose.ts]
BA[bindActionCreators.ts]
end
subgraph "Types (src/types/)"
TA[actions.ts]
TR[reducers.ts]
TS[store.ts]
TM[middleware.ts]
end
subgraph "Utils (src/utils/)"
AT[actionTypes.ts]
IA[isAction.ts]
IP[isPlainObject.ts]
KO[kindOf.ts]
WA[warning.ts]
SO[symbol-observable.ts]
FP[formatProdErrorMessage.ts]
end
CS --> AT
CS --> IP
CS --> KO
CS --> SO
CR --> AT
CR --> IP
CR --> WA
CR --> KO
AM --> CO
BA --> KO
IA --> IP
utils/ 目录是纯内部层。kindOf(用于生成详细错误信息的类型描述)和 warning(带调试器友好 throw 的 console.error 封装)等函数从不对外暴露。formatProdErrorMessage 尤为特殊 —— 它的存在完全是为了在构建时被替换,我们将在本系列的最后一篇文章中详细探讨。
类型系统的层次结构
Redux 的 TypeScript 类型分布在四个文件中,形成一条依赖链,每一层都建立在上一层之上:
flowchart LR
A["actions.ts<br/><i>Action, UnknownAction</i>"] --> B["reducers.ts<br/><i>Reducer, ReducersMapObject</i>"]
B --> C["store.ts<br/><i>Store, StoreEnhancer, Dispatch</i>"]
C --> D["middleware.ts<br/><i>Middleware, MiddlewareAPI</i>"]
基础是 Action<T> —— 注意它是 type 而非 interface(原因涉及 TypeScript 的结构兼容性问题),只要求包含一个 type: string 属性:
UnknownAction 在此基础上添加了索引签名 [extraProps: string]: unknown,而已废弃的 AnyAction 则使用 any 替代。Reducer 类型接收 action 和 state:
Store 接口将一切汇聚在一起 —— dispatch、getState、subscribe、replaceReducer,以及 observable 协议:
处于顶端的是 StoreEnhancer,它的 Ext 和 StateExt 泛型参数实现了类型安全的扩展组合。我们将在第 5 篇文章中深入剖析这一机制。
包导出与构建格式
Redux 从单一源码构建出四种不同的 bundle 格式。package.json 中的 exports 字段和若干遗留字段共同决定了不同消费者所获取的格式:
| 字段 | 文件 | 格式 | 适用场景 |
|---|---|---|---|
exports["."].import |
dist/redux.mjs |
ESM | 现代打包工具(Vite、webpack 5、Rollup) |
exports["."].default |
dist/cjs/redux.cjs |
CJS | Node.js require()、旧版工具链 |
module |
dist/redux.legacy-esm.js |
Legacy ESM(.js) | Webpack 4(能读取 module 字段,但不支持 .mjs) |
| (仅构建产物) | dist/redux.browser.mjs |
Browser ESM(压缩版) | 直接通过 <script type="module"> 使用 |
flowchart TD
SRC["src/index.ts"] --> TSUP["tsup / esbuild"]
TSUP --> ESM["redux.mjs<br/><i>ESM, dev+prod guards</i>"]
TSUP --> LEGACY["redux.legacy-esm.js<br/><i>ESM for Webpack 4</i>"]
TSUP --> BROWSER["redux.browser.mjs<br/><i>ESM, minified, prod-only</i>"]
TSUP --> CJS["cjs/redux.cjs<br/><i>CommonJS</i>"]
package.json 第 84 行的 sideEffects: false 声明至关重要 —— 它告诉打包工具所有导出都可以安全地进行 tree-shaking。如果你只导入了 compose,打包工具就可以将 createStore、combineReducers 等所有其他内容全部裁掉。
提示: browser bundle 在构建时会将
process.env.NODE_ENV硬编码为"production"并进行压缩。这意味着所有仅用于开发环境的代码路径(详细错误信息、kindOf的富类型名称、意外 state key 的警告)都会被彻底移除。而标准的 ESM 和 CJS 构建则保留了process.env.NODE_ENV检查,交由下游打包工具自行处理。
核心架构思想:闭包,而非类
如果你用过 Redux,或许会以为 store 是某个类的实例。但事实并非如此。createStore 是一个函数,返回的是一个普通对象 —— store 的"私有状态"存活在闭包变量中,而不是挂在 this 上:
六个 let 声明,这就是一个 Redux store 的全部私有状态:
flowchart TD
subgraph "Closure Scope (private)"
CR[currentReducer]
CST[currentState]
CL[currentListeners]
NL[nextListeners]
LIC[listenerIdCounter]
ID[isDispatching]
end
subgraph "Returned Object (public)"
D["dispatch()"]
S["subscribe()"]
G["getState()"]
R["replaceReducer()"]
O["[Symbol.observable]()"]
end
D --> CR
D --> CST
D --> ID
D --> CL
D --> NL
G --> CST
S --> NL
S --> LIC
R --> CR
返回对象上的方法 —— dispatch、subscribe、getState、replaceReducer 以及 observable 访问器 —— 都通过闭包引用这些变量。没有原型链,不需要担心 this 绑定,外部代码也无法绕过既定接口直接访问私有状态。
这一设计选择带来了切实的好处:
- 无需担心
this绑定问题。 你可以直接解构const { dispatch, getState } = store,方法依然正常工作,因为它们引用的是闭包变量,而非this。 - 真正意义上的封装。 与私有类字段不同(后者可以在开发工具中被检查,甚至被黑科技绕过),闭包变量在外部是真正无法访问的。
- enhancer 的组合更加简洁。 由于 store 是普通对象,enhancer 可以通过展开运算符覆盖单个方法,而无需继承子类:
{ ...store, dispatch: enhancedDispatch }。
这种模式 —— 返回一个普通对象,其方法通过闭包持有可变变量 —— 是 Redux 最重要的设计决策。整个 enhancer 和中间件系统都建立在这一基础之上,我们将从下一篇文章开始深入探索。
下一步
我们已经完成了全局地图的绘制:17 个文件、9 个导出、分层的类型系统、多格式构建,以及基于闭包的架构。下一篇文章将深入库的核心 —— createStore.ts —— 逐行解读这个长达 500 行的函数:参数的处理逻辑、dispatch 的执行流程、双 Map 监听器快照模式,以及让中间件成为可能的 enhancer 短路机制。