Read OSS

25 行代码读懂 applyMiddleware:中间件管道与 Store 增强

高级

前置知识

  • 第 1-3 篇文章
  • 高阶函数与闭包
  • 对中间件概念有一定了解(Express、Koa 等)

25 行代码读懂 applyMiddleware:中间件管道与 Store 增强

经过前几篇的铺垫,我们已经对 Redux 的核心机制有了扎实的认识:基于闭包的 store、dispatch 循环、reducer 组合以及 compose 工具函数。现在来看最关键的部分——正是这个机制,让 Redux 从一个简单的状态容器,演变成了一个可扩展的平台:applyMiddleware

整个实现只有 25 行运行时代码,却藏着 JavaScript 开源世界中最精妙的闭包技巧之一。读懂它,你就能明白 Redux 中间件为何能成为 React 应用处理副作用的主流模式。

StoreEnhancer 模式

在深入 applyMiddleware 之前,先理解它所实现的抽象:StoreEnhancer。正如第 2 篇所介绍的,当 createStore 接收到 enhancer 时,会提前走一条特殊路径:

src/createStore.ts#L131-L144

调用方式是 enhancer(createStore)(reducer, preloadedState)。enhancer 是一个高阶函数,它:

  1. 接收 createStore 作为参数
  2. 返回一个新的"增强版" createStore
  3. 这个增强版 createStore 可以调用原始版本、修改返回的 store,并将其返回
flowchart TD
    A["enhancer(createStore)"] --> B["enhancedCreateStore"]
    B --> C["enhancedCreateStore(reducer, preloadedState)"]
    C --> D["Calls original createStore internally"]
    D --> E["Gets base store"]
    E --> F["Modifies/wraps store methods"]
    F --> G["Returns enhanced store"]

类型定义把这种组合方式表达得非常清晰:

src/types/store.ts#L220-L232

StoreEnhancer<Ext, StateExt> 有两个类型参数:Ext(添加到 store 的方法或属性)和 StateExt(添加到 state 的属性)。当多个 enhancer 组合时,这些类型会通过交叉类型不断累积:NextExt & ExtNextStateExt & StateExt

applyMiddleware 逐行解读

下面是完整的运行时实现:

src/applyMiddleware.ts#L53-L77

让我们一步步拆解:

sequenceDiagram
    participant App
    participant applyMiddleware
    participant createStore
    participant Middleware A
    participant Middleware B
    participant compose

    App->>applyMiddleware: applyMiddleware(mwA, mwB)
    Note over applyMiddleware: Returns a StoreEnhancer
    applyMiddleware->>createStore: createStore(reducer, preloadedState)
    createStore-->>applyMiddleware: base store

    Note over applyMiddleware: Create throwing dispatch<br/>(safety during setup)

    applyMiddleware->>Middleware A: mwA({ getState, dispatch })
    Middleware A-->>applyMiddleware: chainA (next => action => ...)
    applyMiddleware->>Middleware B: mwB({ getState, dispatch })
    Middleware B-->>applyMiddleware: chainB (next => action => ...)

    applyMiddleware->>compose: compose(chainA, chainB)(store.dispatch)
    compose-->>applyMiddleware: composed dispatch

    Note over applyMiddleware: Reassign dispatch variable!

    applyMiddleware-->>App: { ...store, dispatch: composedDispatch }

第一步:创建基础 store(第 57 行)。applyMiddleware 调用原始的 createStore,且不传入任何 enhancer——因为它本身就是 enhancer。

第二步:创建一个会抛出错误的 dispatch(第 58-63 行)。在中间件初始化阶段,此时链条还未组装完成,直接 dispatch 是不安全的。因此 dispatch 初始指向一个会抛出错误的函数,防止出现如下测试所覆盖的那类 bug:

test/applyMiddleware.spec.ts#L9-L20

第三步:构建 middlewareAPI(第 65-68 行)。每个中间件都会收到 { getState, dispatch }。关键在于,这里的 dispatch 不是上面那个会抛错的占位函数,而是一个包装函数,它会调用当前 dispatch 变量所指向的内容。

第四步:初始化每个中间件(第 69 行)。用 API 调用 middleware(middlewareAPI),得到形如 next => action => ... 的函数。

第五步:组合中间件链(第 70 行)。compose(...chain)(store.dispatch) 从右到左将中间件串联起来,以原始 store.dispatch 作为最内层的 next

第六步:重新赋值 dispatch(第 70 行)。let dispatch 变量被重新赋值为组合后的链。

第七步:返回增强后的 store(第 72-75 行)。展开基础 store,并用组合后的 dispatch 覆盖原有的 dispatch

中间件签名:store => next => action

每个 Redux 中间件都是一个三层柯里化函数。下面用测试辅助文件中的 thunk 中间件来逐层说明:

test/helpers/middleware.ts#L1-L9

flowchart TD
    subgraph "Layer 1: Setup"
        A["({ dispatch, getState }) =>"]
    end
    subgraph "Layer 2: Chain building"
        B["(next) =>"]
    end
    subgraph "Layer 3: Action handling"
        C["(action) => ..."]
    end
    A --> B --> C

    D["middlewareAPI"] --> A
    E["next middleware's<br/>action handler<br/>(or store.dispatch)"] --> B
    F["Dispatched action"] --> C

第一层{ dispatch, getState } =>)在 applyMiddleware 执行期间只调用一次。中间件通过闭包捕获这个 API。

第二层next =>)在 compose 执行期间只调用一次。next 是下游的 dispatch——链中下一个中间件,或者(对最后一个中间件而言)原始的 store.dispatch

第三层action =>)在每次 dispatch 时都会调用,是实际处理 action 的"热路径"函数。

"洋葱模型"意味着 action 从外向内穿过各层,再从内向外返回:

flowchart LR
    A["Action dispatched"] --> B["Middleware A<br/>(outer)"]
    B --> C["Middleware B<br/>(inner)"]
    C --> D["store.dispatch<br/>(base)"]
    D --> E["Reducer"]
    E --> F["New state"]

每个中间件都可以:

  • 在传给 next 之前修改 action
  • 完全拦截 action(不调用 next
  • 调用 next 之后再做一些后续处理
  • 通过第一层拿到的 dispatch 来派发额外的 action

闭包技巧:递归 dispatch

applyMiddleware 最精妙的地方就在这里。再看一眼第 65-68 行:

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

这里的 dispatch 不是那个会抛错的占位函数,而是一个包装函数,它调用的是 dispatch 这个 let 变量。在初始化阶段,该变量指向抛错函数;但等到任何中间件真正调用 middlewareAPI.dispatch 时,第 70 行早已将 dispatch 重新赋值为完整的组合链。

sequenceDiagram
    participant Thunk
    participant middlewareAPI.dispatch
    participant dispatch variable
    participant Full Chain

    Note over dispatch variable: Initially: throwing fn
    Note over dispatch variable: After compose: full chain

    Thunk->>middlewareAPI.dispatch: dispatch(someAction)
    middlewareAPI.dispatch->>dispatch variable: dispatch(someAction)
    dispatch variable->>Full Chain: Goes through ALL middleware
    Full Chain->>Thunk: Result

这意味着,当 thunk 内部调用 dispatch(anotherAction) 时,这个 action 会流经整条中间件链,而不是从当前位置往后的部分。正是这一点,让递归 dispatch 成为可能:一个 thunk 可以 dispatch 另一个 thunk,后者再 dispatch 一个普通 action,它们都会完整地经过每一个中间件。

如果 middlewareAPI.dispatch 直接捕获的是 store.dispatch(或某个固定时刻的组合 dispatch),那么 thunk 嵌套 thunk 时就会绕过中间件链。可变的 let 变量加上闭包,才是整个机制能正常运作的关键。

提示: 这正是 middlewareAPI 中的 dispatch 要写成 (action, ...args) => dispatch(action, ...args) 这种包装形式,而不是直接赋值 dispatch 的原因。直接引用会在赋值那一刻就捕获变量的当前值;而包装函数会把查找推迟到实际调用时,每次都读取最新的值。

完整示例:Thunk 中间件的执行流程

来追踪一个完整的调用过程。考虑如下测试场景:

test/applyMiddleware.spec.ts#L47-L65

store 使用了两个中间件:一个 spy 和 thunk 中间件。当 addTodoAsync 被 dispatch 时,它返回的是一个函数(即"thunk")。执行流程如下:

  1. store.dispatch(addTodoAsync('Use Redux')) 调用组合后的 dispatch
  2. spy 中间件收到这个函数类型的 action,执行 spyOnMethods(action),然后调用 next(action)
  3. thunk 中间件收到该函数,检测到 typeof action === 'function',于是调用 action(dispatch, getState)
  4. 在 thunk 内部,最终会调用 dispatch(addTodo('Use Redux'))——这会再次从头经过整条链(spy → thunk → store.dispatch)
  5. 这一次 action 是普通对象,thunk 调用 next(action),最终到达基础 store.dispatch

spy 被调用了两次(一次处理 thunk,一次处理普通 action),这证明了递归 dispatch 确实会流经完整的中间件链。

第 119-151 行的测试进一步验证了传给 dispatch 的额外参数会在整条链中原样保留——这对一些较为特殊的中间件模式来说是个重要细节。

下一篇

25 行代码,却构建出了 JavaScript 生态中最强大的扩展机制之一。核心要素是:闭包(可变的 dispatch 变量)、高阶函数(三层柯里化的中间件签名)以及函数组合(compose 将中间件串联起来)。

下一篇,我们将从运行时行为转向类型系统。Redux 的 TypeScript 类型出人意料地精巧——从 reducer map 推断 state 类型的条件类型、UnknownIfNonSpecific 技巧,以及无需运行代码即可验证类型推断的类型级测试。