
一句话:中间件是「在 dispatch 与 reducer 之间插入的一层」,用来处理「非普通对象」的 action(如函数、Promise)或加入横切逻辑(日志、埋点、持久化);它通过柯里化的函数签名
store => next => action => {}串联成链。
一、为什么需要中间件
// Redux 的 dispatch 默认只接受「普通对象」action
store.dispatch({ type: 'todos/add', payload: todo }); // ✅
store.dispatch(() => {}); // ❌ 报错
store.dispatch(Promise.resolve()); // ❌ 报错
// 报错信息:
// Actions must be plain objects. Use custom middleware for async actions.
问题:异步操作怎么办?
❌ 组件里直接写异步(问题多):
useEffect(() => {
api.getTodos().then((data) => store.dispatch({ type: 'todos/set', payload: data }));
}, []);
→ 逻辑散落在组件里;多个组件要重复写;无法复用;难以测试
✅ 用中间件把「异步逻辑」从组件里抽出来:
dispatch(fetchTodos()) ← 派发一个「函数」
→ thunk 中间件拦截 → 执行这个函数(在里面做请求与 dispatch)
中间件解决的其它问题:
· 日志:记录每次 dispatch 的 action 与前后状态
· 埋点:所有 action 自动上报
· 持久化:状态变化时写入 localStorage
· 崩溃上报:捕获 action 处理中的错误
二、中间件原理
// 一个中间件的标准签名(三层柯里化)
const myMiddleware = (store) => (next) => (action) => {
// ① store:{ getState, dispatch }(注意 dispatch 是【被增强过的】)
// ② next:调用链中的下一个中间件(或最终的 store.dispatch)
// ③ action:本次派发的 action
console.log('before', action);
const result = next(action); // ⭐ 交给下一个中间件 / reducer
console.log('after', store.getState());
return result; // ⭐ 必须返回(调用方可能需要)
};
为什么要柯里化三层?
① (store) => ... —— Redux 创建 store 时把 store 传进来(只需一次)
② (next) => ... —— applyMiddleware 在「组装中间件链」时把 next 传进来(只需一次)
③ (action) => ... —— 每次 dispatch 时执行
好处:
· ① 和 ② 只在初始化时执行一次(可以在这里做「初始化工作」)
· ③ 是「每次调用都执行」的热路径
· next 的引用在链组装后就固定了 → 每次 dispatch 只是沿着链传递
// 简化版 applyMiddleware(理解「链」是怎么组装的)
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState);
// ⭐ dispatch 初始为「只能抛错的占位」(因为中间件里不能直接用它)
let dispatch = () => {
throw new Error('Dispatching while constructing your middleware is not allowed.');
};
const middlewareAPI = {
getState: store.getState,
dispatch: (action, ...args) => dispatch(action, ...args), // 延迟绑定
};
// 每个中间件先「吃」掉 store(第一层)
const chain = middlewares.map((m) => m(middlewareAPI));
// ⭐ 从右到左组合,得到最终增强的 dispatch
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
};
}
function compose(...fns) {
if (fns.length === 0) return (arg) => arg;
if (fns.length === 1) return fns[0];
return fns.reduce((a, b) => (...args) => a(b(...args)));
}
三、手写几个中间件
// ① 日志中间件
const logger = (store) => (next) => (action) => {
console.group(action.type);
console.log('dispatching:', action);
console.log('prev state:', store.getState());
const result = next(action);
console.log('next state:', store.getState());
console.groupEnd();
return result;
};
// ② 崩溃上报中间件(捕获 action 处理中的异常)
const crashReporter = (store) => (next) => (action) => {
try {
return next(action);
} catch (err) {
console.error('Caught an exception!', err);
reportError(err, { action, state: store.getState() });
throw err; // 重新抛出,不吞掉错误
}
};
// ③ 埋点中间件(只上报感兴趣的 action)
const analytics = (store) => (next) => (action) => {
if (typeof action === 'object' && action.meta?.track) {
track(action.type, action.payload);
}
return next(action);
};
// ④ 持久化中间件(防抖写入)
const persistence = (store) => {
let timer;
return (next) => (action) => {
const result = next(action);
clearTimeout(timer);
timer = setTimeout(() => {
const state = store.getState();
localStorage.setItem('app-state', JSON.stringify({ cart: state.cart }));
}, 300);
return result;
};
};
// ⑤ 组装(applyMiddleware 的顺序 = 执行顺序)
const store = createStore(
rootReducer,
applyMiddleware(logger, crashReporter, analytics, persistence)
);
四、thunk:最常用的异步中间件
// thunk 的核心逻辑(非常短)
const thunk = ({ dispatch, getState }) => (next) => (action) => {
if (typeof action === 'function') {
// ⭐ 如果 action 是函数,就调用它(把 dispatch 与 getState 传进去)
return action(dispatch, getState);
}
return next(action); // 否则交给下一个中间件
};
// 用法:action creator 返回一个函数
// ① 基础:条件式 dispatch(先读状态再决定)
const addTodoIfNotExists = (text) => (dispatch, getState) => {
const { todos } = getState();
if (todos.some((t) => t.text === text)) return; // ⭐ 能读状态
dispatch({ type: 'todos/add', payload: { text } });
};
// ② 异步请求(loading → success / error)
const fetchTodos = (userId) => async (dispatch, getState) => {
dispatch({ type: 'todos/loading' });
try {
const data = await api.getTodos(userId);
dispatch({ type: 'todos/success', payload: data });
} catch (err) {
dispatch({ type: 'todos/error', payload: String(err) });
}
};
// ③ 按需读取最新状态(避免组件传参)
const saveDraft = () => async (dispatch, getState) => {
const { editor } = getState();
await api.saveDraft(editor.content);
dispatch({ type: 'editor/markSaved' });
};
// ④ 返回 Promise(让组件可以 await 结果)
const submitOrder = () => async (dispatch, getState) => {
const order = await api.submit(getState().cart);
dispatch({ type: 'cart/clear' });
return order; // ⭐ 组件能拿到返回值
};
// 组件里使用
function TodoList() {
const dispatch = useDispatch();
const { items, status } = useAppSelector((s) => s.todos);
useEffect(() => {
dispatch(fetchTodos('me'));
}, [dispatch]);
return status === 'loading' ? <Skeleton /> : <List items={items} />;
}
// 需要拿到结果时
const handleSubmit = async () => {
const order = await dispatch(submitOrder()).unwrap?.() ?? await dispatch(submitOrder());
navigate(`/order/${order.id}`);
};
五、thunk 的三态建模
// ① 用「三个 action」表达异步的三个阶段(最传统的写法)
const initialState = { items: [], status: 'idle', error: null };
function todosReducer(state = initialState, action) {
switch (action.type) {
case 'todos/loading':
return { ...state, status: 'loading', error: null };
case 'todos/success':
return { ...state, status: 'success', items: action.payload };
case 'todos/error':
return { ...state, status: 'error', error: action.payload };
default:
return state;
}
}
⚠️ 注意:不要用「多个布尔值」表达状态
❌ { isLoading: false, isError: false, isSuccess: false, data: null }
→ 会产生「同时 loading 且 error」这类非法组合
✅ { status: 'idle' | 'loading' | 'success' | 'error', data, error }
用 RTK 时这一套由
createAsyncThunk自动生成(pending/fulfilled/rejected三个 action),见 3.9。
六、其它中间件与选型
| 中间件 | 用途 | 特点 |
|---|---|---|
| redux-thunk | 函数式 action(异步、条件 dispatch) | 极简单(十几行),RTK 内置 |
| redux-saga | 用 generator 编排复杂副作用 | 能力强(可取消、竞态、并发编排),学习曲线陡 |
| redux-observable | 用 RxJS 处理 action 流 | 适合「事件流」类需求,生态较小 |
| redux-persist | 自动持久化 store | 配置化,比手写持久化中间件省事 |
| redux-logger | 打印 action 与状态变化 | 开发环境常用(RTK 的 DevTools 已覆盖大部分) |
选型建议:
① 大多数项目 → thunk(简单够用,RTK 已内置)
② 需要「取消请求、竞态处理、复杂的并发编排」→ saga 或 RTK Query
③ 只是要「请求 + 缓存」→ 直接用 RTK Query 或 TanStack Query(不需要 saga)
七、中间件的执行顺序
applyMiddleware(logger, crashReporter, analytics)
组装后的调用链(dispatch 时的顺序):
dispatch(action)
→ logger(before)
→ crashReporter(try)
→ analytics(上报)
→ store.dispatch(真正的 reducer 处理)
← analytics(after)
← crashReporter(catch)
← logger(after state)
规则:
① 数组中【靠前的】中间件在「进入时」先执行
② 数组中【靠后的】中间件在「退出时」后执行
③ 因此 logger 放在最前面 → 能看到最完整的信息(包括其它中间件的影响)
④ crashReporter 应该放在【靠前】的位置 → 能捕获后面所有中间件的异常
八、常见坑
// ① 忘记写 return next(action)
const bad = (store) => (next) => (action) => {
next(action); // ❌ 没有 return → 调用方拿不到返回值
};
// ② 在中间件里直接调用 store.dispatch 造成「循环」
const bad2 = (store) => (next) => (action) => {
store.dispatch({ type: 'something' }); // ⚠️ 会重新走一遍整个链(可能死循环)
return next(action);
};
// ✅ 需要「再派发」时用 next 或明确的「不拦截」逻辑
// ③ 中间件里「吞掉」action 而不调 next
const swallow = (store) => (next) => (action) => {
if (action.type === 'blocked') return; // ⚠️ 这个 action 不会到达 reducer(有时是故意的)
return next(action);
};
// ④ 在中间件构造阶段(第一、二层)调用 dispatch
const bad3 = (store) => {
store.dispatch({ type: 'init' }); // ❌ 抛错:Dispatching while constructing
return (next) => (action) => next(action);
};
// ⑤ 认为 thunk 只能做异步
dispatch((d, g) => { if (g().cond) d({ type: 'x' }); }); // ✅ 条件式同步 dispatch 也常用
// ⑥ thunk 里忘记返回 Promise → 组件无法 await
const noReturn = () => async (dispatch) => { await api.post(); }; // ⚠️ 没有 return
// ✅ 需要结果时显式 return
// ⑦ 用 thunk 处理「服务端数据缓存」→ 手写缓存/失效(应该用 TanStack Query)
// ⑧ 中间件顺序写错(crashReporter 放在最后 → 捕获不到前面的异常)
// ⑨ 在中间件里做「重计算」(每次 dispatch 都执行)
// ⑩ 用 saga 做「只发请求」的活(过度设计,thunk 就够)
// ⑪ 忘记把中间件加进 applyMiddleware → 对应功能静默失效
九、面试延伸
- 「Redux 中间件是什么?解决什么问题?」
中间件是「插在dispatch与reducer之间的一层逻辑」。它解决两类问题:① 支持非普通对象的 action——Redux 的dispatch默认只接受普通对象,异步操作无法直接派发,中间件可以拦截「函数」或「Promise」并自行处理(如 thunk);② 插入横切关注点——日志、埋点、崩溃上报、持久化、状态校验等,这些逻辑如果写在每个 action creator 里会重复且难维护。 -
「中间件的签名为什么是三层柯里化?」
(store) => (next) => (action) => {}的三层对应三个不同时机:① 第一层(store)在 store 创建时执行一次(可以拿到getState与增强后的dispatch);② 第二层(next)在 组装中间件链时执行一次(next指向链中的下一个中间件);③ 第三层(action)在每次 dispatch 时执行(热路径)。这样可以「在初始化时做一次性准备、在每次派发时只做必要的事」。 -
「
next和store.dispatch有什么区别?」
next是「链中的下一个中间件」——调用它表示「我这个中间件处理完了,交给下一个」;store.dispatch是「从链的头部重新开始」——调用它会再次经过所有中间件(包括当前这个),因此可能导致重复处理或死循环。所以中间件里应该调next(action),只有「想额外派发一个新 action 并让它走完整流程」时才用dispatch。 -
「thunk 的原理是什么?为什么能处理异步?」
thunk 中间件的逻辑非常短:如果action是函数,就调用它并把dispatch与getState传进去;否则交给next。因此「异步」并不是 thunk 的能力,而是「你在那个函数里怎么写」——你可以await一个请求、然后再dispatch结果。thunk 的价值是「把异步编排从组件里抽出来」,让组件只负责dispatch(fetchTodos())。 -
「中间件的执行顺序是怎样的?」
applyMiddleware(a, b, c)会按数组顺序组合成a(b(c(store.dispatch)))。因此 dispatch 时:「进入阶段」按数组顺序(a → b → c → reducer),「退出阶段」反向(reducer → c → b → a)。这也决定了放置策略:logger放最前(能看到完整信息)、crashReporter放靠前(能捕获后面所有中间件的异常)。 -
「thunk 和 saga 怎么选?」
按复杂度选:thunk(RTK 内置)——极简、适合「发请求 → dispatch 结果」「条件式 dispatch」这类常见需求,占 90% 场景;saga——基于 generator,能表达「取消、竞态(race/takeLatest)、并发编排(all)、防抖节流」等复杂副作用,适合「大量交错的异步流程」,但学习成本高、代码量大。如果只是「请求 + 缓存」,两者都不需要——直接用 RTK Query 或 TanStack Query。 -
「中间件能修改 action 吗?」
能。中间件可以在调用next之前替换 action(如把「函数」替换成「普通对象」),也可以包裹它(如加meta、加时间戳、加 traceId)。这正是 thunk 的做法——把「函数 action」拦截下来执行,并不把函数本身传给next(因为 reducer 只能处理普通对象)。不过要谨慎:随意改造 action 会让数据流难以追踪。 -
「为什么『在中间件里直接 dispatch』是危险的?」
因为store.dispatch是「链的入口」——在中间件里调用它,会让新 action 重新走一遍所有中间件(包括当前这个)。如果那个 action 又被同一个中间件再次 dispatch,就会无限递归(栈溢出)。正确做法是:① 需要「继续传递」用next(action);② 需要「额外派发」时,仔细确认它不会被当前中间件再次拦截(或加条件判断)。
一句话速记
中间件是「dispatch 与 reducer 之间的一层」,用来支持非普通对象的 action(异步)与插入横切逻辑(日志/埋点/持久化);签名是
(store) => (next) => (action) => {}(三层对应「初始化一次 / 组装时一次 / 每次派发」);thunk 的原理就是「action 是函数就调用它,把dispatch与getState传进去」,异步能力来自你在那个函数里怎么写;中间件里要调next(action)而不是store.dispatch(否则会重走全链甚至死循环);applyMiddleware(a, b, c)的进入顺序与数组一致、退出顺序相反(logger放最前、crashReporter放靠前);只做「请求 + 缓存」的话 thunk/saga 都不需要,直接用查询库。



最新评论
读过书不知道欧·亨利的人少。教科书上选文有
这小生活不错呀
不错,必须顶一下!
看着你还在坚持,很好
看来忙了也没时间更新博客了
NIce。学习了。。。。
网站不错!!!!
简洁实用,好文章!