Redux中间件是连接同步reducer与各类副作用需求的桥梁。默认情况下,createStore返回的dispatch方法只会检查action是否为普通对象并且带有type属性,然后同步调用reducer计算新状态。如果开发者试图传入一个函数来延迟执行某个任务,例如在数据请求完成后再派发真实action,这套默认实现会直接报出Actions must be plain objects错误。中间件机制没有改动reducer和state结构,而是通过包装dispatch,让本来只能处理对象的方法获得执行异步逻辑、记录日志、捕获异常等扩展能力。下面进入源码细节。

一、默认dispatch的边界与限制
要理解中间件为什么以这种形式出现,先要看清原生dispatch做了什么。Redux的createStore在内部维护当前状态、当前reducer和监听器列表,dispatch负责派发动作、更新状态并通知订阅者。一个简化后的核心实现大致如下。
function createStore(reducer, preloadedState, enhancer) {
if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
enhancer = preloadedState;
preloadedState = undefined;
}
if (typeof enhancer !== 'undefined') {
if (typeof enhancer !== 'function') {
throw new Error('Expected the enhancer to be a function.');
}
return enhancer(createStore)(reducer, preloadedState);
}
let currentReducer = reducer;
let currentState = preloadedState;
let currentListeners = [];
let nextListeners = currentListeners;
let isDispatching = false;
function getState() {
if (isDispatching) {
throw new Error('You may not call store.getState() while the reducer is executing.');
}
return currentState;
}
function dispatch(action) {
if (typeof action !== 'object' || action === null) {
throw new Error('Actions must be plain objects. Use custom middleware for async actions.');
}
if (typeof action.type === 'undefined') {
throw new Error('Actions may not have an undefined "type" property.');
}
if (isDispatching) {
throw new Error('Reducers may not dispatch actions.');
}
try {
isDispatching = true;
currentState = currentReducer(currentState, action);
} finally {
isDispatching = false;
}
const listeners = currentListeners = nextListeners;
for (let i = 0; i < listeners.length; i++) {
const listener = listeners[i];
listener();
}
return action;
}
return {
dispatch,
getState,
subscribe,
replaceReducer
};
}
这段代码中最值得关注的是dispatch内部的校验逻辑。它先判断action必须是对象且不能为null,接着判断action.type不能是undefined。这意味着函数、Promise、字符串等非对象值都会被直接拒绝。更关键的是,dispatch会同步执行reducer,并在执行期间锁定isDispatching标志,防止reducer内部再次派发动作造成死循环。这种设计保证了状态变更的可预测性,但也把异步请求、定时器等副作用完全挡在Redux流程之外。
如果想在请求完成后再派发一个真正的action,常规做法是先调用store.dispatch发送一个pending动作,然后在回调中再手动调用store.dispatch发送success或failure动作。这种写法能够工作,但会让组件层和数据层耦合大量重复逻辑。中间件解决的正是这个问题,它不改变reducer,而是允许开发者定义一个接收store工具能力的函数,在action抵达原始dispatch之前或之后执行额外操作。
二、applyMiddleware与compose的组合机制
Redux官方提供的applyMiddleware函数是一个store增强器。createStore检测到enhancer参数存在时,就不再自行创建store,而是把创建权交给enhancer。applyMiddleware拿到createStore后,先创建一个临时的dispatch占位函数,然后为每个中间件注入getState和dispatch接口,再通过compose把中间件链组合成一个新的dispatch函数。
function compose(...funcs) {
if (funcs.length === 0) {
return arg => arg;
}
if (funcs.length === 1) {
return funcs[0];
}
return funcs.reduce((a, b) => (...args) => a(b(...args)));
}
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args);
let dispatch = () => {
throw new Error('Dispatching while constructing your middleware is not allowed.');
};
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...rest) => dispatch(action, ...rest)
};
const chain = middlewares.map(middleware => middleware(middlewareAPI));
dispatch = compose(...chain)(store.dispatch);
return {
...store,
dispatch
};
};
}
先看compose。它接收一组函数,使用reduce从右到左执行组合。当传入多个中间件时,compose会把最后一个中间件作为最内层,把第一个中间件作为最外层。组合后的结果依然是一个函数,这个函数接收原始store.dispatch,并返回经过所有中间件包装后的新dispatch。这种从右到左的组合方式形成了洋葱结构,请求从前到后经过每一层中间件,响应则从后到前依次返回。
applyMiddleware内部有几个容易忽略的细节。第一,middlewareAPI中的dispatch并不是原始store.dispatch,而是一个闭包引用。中间件在初始化阶段拿到的dispatch并不是最终包装后的dispatch,而是会在后续指向新生成的dispatch变量。这样设计是为了避免中间件在构建阶段调用dispatch导致状态错乱。第二,每个中间件都被调用一次并传入middlewareAPI,返回值是一个接收next函数的高阶函数。compose负责把这些next连接起来,每个next指向链条中更内层的dispatch实现。第三,最终返回的对象使用展开运算符保留store原有的getState、subscribe等方法,但用新dispatch覆盖了原始dispatch,因此调用方对store.dispatch的引用会指向增强后的版本。
中间件的三层柯里化结构也可以从这个源码中直接推导出来。第一层接收middlewareAPI,用于注入getState和dispatch;第二层接收next,用于表示下游处理函数;第三层接收action,用于处理具体的业务动作。如果中间件不调用next,action就不会继续向下传递,因此中间件具备拦截能力。如果中间件在调用next前后添加日志,就形成了日志记录。如果中间件判断action是一个函数并自行执行,就形成了异步thunk。
三、自定义中间件与执行顺序验证
理解源码之后,编写一个自定义中间件就变得非常直观。下面示例同时定义两个中间件,一个记录派发前后的日志,另一个处理函数类型的action。
const loggerMiddleware = store => next => action => {
console.log('dispatching', action);
const result = next(action);
console.log('next state', store.getState());
return result;
};
const thunkMiddleware = store => next => action => {
if (typeof action === 'function') {
return action(store.dispatch, store.getState);
}
return next(action);
};
loggerMiddleware在调用next之前打印当前action,在next返回后通过store.getState读取最新状态。thunkMiddleware检查action是否为函数,如果是函数就执行该函数,并把增强后的store.dispatch和store.getState传给它,否则把action交给下游。这两个中间件分别代表了同步增强和异步处理两种典型场景。
执行顺序可以通过组合这两个中间件来观察。假设又增加两个只输出日志的中间件,分别命名为firstMiddleware和secondMiddleware,并在createStore时按顺序传入。
const firstMiddleware = store => next => action => {
console.log('first before');
const result = next(action);
console.log('first after');
return result;
};
const secondMiddleware = store => next => action => {
console.log('second before');
const result = next(action);
console.log('second after');
return result;
};
const store = createStore(
reducer,
applyMiddleware(firstMiddleware, secondMiddleware)
);
store.dispatch({ type: 'INCREMENT' });
// 输出:
// first before
// second before
// second after
// first after
从输出可以清楚看到,第一个传入的中间件位于最外层,第二个位于内层。action先进入firstMiddleware,再进入secondMiddleware,最后到达原始dispatch更新状态。return值则按照完全相反的顺序回到最外层。如果某个中间件在调用next之前返回,下游逻辑不会执行;如果在调用next之后返回,上游中间件可以继续处理结果。这种结构非常适合实现日志、权限校验、错误边界、请求去重等复杂能力。
四、现代Redux中的中间件配置与注意事项
在实际项目中,现在更常见的是使用Redux Toolkit提供的configureStore。它内部已经默认包含了一些常用中间件,例如redux-thunk和开发环境下的状态不可变检查、序列化检查等。用户只需要通过middleware选项添加额外中间件,不再需要手动调用applyMiddleware。
import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';
const store = configureStore({
reducer: rootReducer,
middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger)
});
但是读applyMiddleware源码仍然有实际价值。当排查中间件不生效、action数量异常、异步状态丢失等问题时,理解函数组合方向可以帮助快速定位。一个常见错误是把日志中间件放在异步中间件外层,但异步中间件又在处理函数action时手动调用了store.dispatch。此时日志中间件只能看到最初的函数action,看不到函数内部派发的真实action。原因很简单,函数内部调用的是增强后的store.dispatch,但这次调用会重新从最外层进入整个中间件链。如果日志中间件没有记录这个新的真实action,开发者就可能在控制台里只看到函数对象,误以为异步派发被吞掉。
另一个需要注意的地方是中间件的初始化时序。applyMiddleware在创建store时只会执行中间件的第一层和第二层,也就是middleware => next => action中的前两层。真正处理每个action的第三层函数是在组合完成后才被调用。因此不建议在中间件初始化阶段依赖最终dispatch行为,也不要在初始化阶段尝试读取业务状态并立即派发动作,否则很容易触发Dispatching while constructing错误。中间件应该保持纯函数式的结构,把可变逻辑放到最内层的action处理器中。
从源码层面看,Redux中间件并不是一个神秘的功能,它只是高阶函数和函数组合的巧妙应用。通过包装dispatch、注入store接口、连接next链路,它让原本只能同步处理普通对象的dispatch具备了近乎无限的扩展空间。掌握这套机制后,无论是使用redux-thunk处理异步,还是编写自定义日志、崩溃上报、权限拦截中间件,都能更清楚地判断每一步的数据流向和执行边界。
Redux中间件JavaScript状态管理源码解析修改时间:2026-09-29 00:24:08