Read OSS

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)中。我们先来看看完整内容:

src/index.ts#L1-L50

一共 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 createStorelegacy_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 属性:

src/types/actions.ts#L19-L21

UnknownAction 在此基础上添加了索引签名 [extraProps: string]: unknown,而已废弃的 AnyAction 则使用 any 替代。Reducer 类型接收 action 和 state:

src/types/reducers.ts#L30-L34

Store 接口将一切汇聚在一起 —— dispatchgetStatesubscribereplaceReducer,以及 observable 协议:

src/types/store.ts#L81-L165

处于顶端的是 StoreEnhancer,它的 ExtStateExt 泛型参数实现了类型安全的扩展组合。我们将在第 5 篇文章中深入剖析这一机制。

包导出与构建格式

Redux 从单一源码构建出四种不同的 bundle 格式。package.json 中的 exports 字段和若干遗留字段共同决定了不同消费者所获取的格式:

package.json#L26-L36

字段 文件 格式 适用场景
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,打包工具就可以将 createStorecombineReducers 等所有其他内容全部裁掉。

提示: browser bundle 在构建时会将 process.env.NODE_ENV 硬编码为 "production" 并进行压缩。这意味着所有仅用于开发环境的代码路径(详细错误信息、kindOf 的富类型名称、意外 state key 的警告)都会被彻底移除。而标准的 ESM 和 CJS 构建则保留了 process.env.NODE_ENV 检查,交由下游打包工具自行处理。

核心架构思想:闭包,而非类

如果你用过 Redux,或许会以为 store 是某个类的实例。但事实并非如此。createStore 是一个函数,返回的是一个普通对象 —— store 的"私有状态"存活在闭包变量中,而不是挂在 this 上:

src/createStore.ts#L146-L153

六个 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

返回对象上的方法 —— dispatchsubscribegetStatereplaceReducer 以及 observable 访问器 —— 都通过闭包引用这些变量。没有原型链,不需要担心 this 绑定,外部代码也无法绕过既定接口直接访问私有状态。

这一设计选择带来了切实的好处:

  1. 无需担心 this 绑定问题。 你可以直接解构 const { dispatch, getState } = store,方法依然正常工作,因为它们引用的是闭包变量,而非 this
  2. 真正意义上的封装。 与私有类字段不同(后者可以在开发工具中被检查,甚至被黑科技绕过),闭包变量在外部是真正无法访问的。
  3. enhancer 的组合更加简洁。 由于 store 是普通对象,enhancer 可以通过展开运算符覆盖单个方法,而无需继承子类:{ ...store, dispatch: enhancedDispatch }

这种模式 —— 返回一个普通对象,其方法通过闭包持有可变变量 —— 是 Redux 最重要的设计决策。整个 enhancer 和中间件系统都建立在这一基础之上,我们将从下一篇文章开始深入探索。

下一步

我们已经完成了全局地图的绘制:17 个文件、9 个导出、分层的类型系统、多格式构建,以及基于闭包的架构。下一篇文章将深入库的核心 —— createStore.ts —— 逐行解读这个长达 500 行的函数:参数的处理逻辑、dispatch 的执行流程、双 Map 监听器快照模式,以及让中间件成为可能的 enhancer 短路机制。