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 时,会提前走一条特殊路径:
调用方式是 enhancer(createStore)(reducer, preloadedState)。enhancer 是一个高阶函数,它:
- 接收
createStore作为参数 - 返回一个新的"增强版"
createStore - 这个增强版
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"]
类型定义把这种组合方式表达得非常清晰:
StoreEnhancer<Ext, StateExt> 有两个类型参数:Ext(添加到 store 的方法或属性)和 StateExt(添加到 state 的属性)。当多个 enhancer 组合时,这些类型会通过交叉类型不断累积:NextExt & Ext、NextStateExt & 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")。执行流程如下:
store.dispatch(addTodoAsync('Use Redux'))调用组合后的 dispatch- spy 中间件收到这个函数类型的
action,执行spyOnMethods(action),然后调用next(action) - thunk 中间件收到该函数,检测到
typeof action === 'function',于是调用action(dispatch, getState) - 在 thunk 内部,最终会调用
dispatch(addTodo('Use Redux'))——这会再次从头经过整条链(spy → thunk → store.dispatch) - 这一次 action 是普通对象,thunk 调用
next(action),最终到达基础store.dispatch
spy 被调用了两次(一次处理 thunk,一次处理普通 action),这证明了递归 dispatch 确实会流经完整的中间件链。
第 119-151 行的测试进一步验证了传给 dispatch 的额外参数会在整条链中原样保留——这对一些较为特殊的中间件模式来说是个重要细节。
下一篇
25 行代码,却构建出了 JavaScript 生态中最强大的扩展机制之一。核心要素是:闭包(可变的 dispatch 变量)、高阶函数(三层柯里化的中间件签名)以及函数组合(compose 将中间件串联起来)。
下一篇,我们将从运行时行为转向类型系统。Redux 的 TypeScript 类型出人意料地精巧——从 reducer map 推断 state 类型的条件类型、UnknownIfNonSpecific 技巧,以及无需运行代码即可验证类型推断的类型级测试。