在React生态里,Redux承担着全局状态管理的角色,但它有一个天生的限制:store的dispatch只能派发同步的plain object,一旦遇到接口请求、定时器这类异步操作,纯Redux就无能为力了。要解决这个问题,就得靠中间件。目前社区里最主流的两个方案是redux-thunk和redux-saga,它们思路完全不同,适用的项目规模也差别很大。这篇文章会把两者的原理、用法和选型思路讲清楚。

一、先弄懂Redux中间件的执行原理
中间件的本质是对store.dispatch的一层增强。Redux在创建store时通过applyMiddleware接收若干中间件,这些中间件会形成一个洋葱模型一样的调用链,每个中间件都有机会在action到达reducer之前对它做处理,处理完再交给下一个中间件。
一个最简单的日志中间件只有几行代码:
const logger = store => next => action => {
console.log('dispatch前:', action);
const result = next(action); // 交给下一个中间件
console.log('dispatch后:', store.getState());
return result;
};
const store = createStore(
reducer,
applyMiddleware(logger)
);
理解这个三级柯里化的签名很关键:store参数让你能拿到getState和dispatch,next指向链中的下一个中间件,最后的action就是当前被派发的动作。当某个中间件不调用next(action)而是自己处理逻辑时,这个action就不会到达reducer,这正是redux-thunk拦截函数型action的原理基础。
二、redux-thunk:最轻量的异步方案
redux-thunk的思路非常直接:正常情况下action必须是一个纯对象,而thunk允许你dispatch一个函数。中间件检测到action是函数时就会调用它,并把dispatch和getState作为参数传入,于是在这个函数里你可以随意发起异步请求,等结果回来后再dispatch普通的同步action去更新状态。
来看一个完整的登录请求示例:
// actionCreator 返回一个函数而非对象
const loginAction = (username, password) => {
return async (dispatch, getState) => {
dispatch({ type: 'LOGIN_REQUEST' });
try {
const res = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ username, password })
});
const data = await res.json();
dispatch({ type: 'LOGIN_SUCCESS', payload: data });
} catch (err) {
dispatch({ type: 'LOGIN_FAILURE', error: err.message });
}
};
};
// 组件中直接 dispatch 这个函数
dispatch(loginAction('admin', '123456'));
这种写法的好处是学习成本几乎为零,业务逻辑一目了然,包体积也只有不到1KB。对于接口数量不多、流程简单的中小项目,thunk完全够用。
但它的短板同样明显:副作用逻辑散落在各个actionCreator里,没有统一的治理手段。比如搜索框防抖场景下,用户快速输入会触发多次请求,thunk无法自动取消上一次未完成的请求,只能自己在组件里用防抖函数或者额外的标记变量去控制,代码很快就变得混乱。此外,多个请求按顺序执行、失败重试、轮询这类复杂流程,用thunk写出来基本是回调嵌套或者手动管理状态机,可读性会持续下降。
三、redux-saga:用Generator驯服复杂副作用
redux-saga换了一个思路:它把所有副作用从组件和actionCreator中抽离出来,集中放到独立的saga文件里,通过Generator函数以同步风格的代码描述异步流程。saga中间件会拦截特定的action,然后执行对应的Generator,遇到yield表达式时暂停,等结果就绪再恢复执行。
几个核心API必须掌握:takeEvery监听某个action并对每次触发都启动一个新任务;takeLatest同样监听,但会自动取消上一次未完成的任务,天然适合搜索请求;call以同步写法调用返回Promise的函数;put相当于saga内部的dispatch;select用于从store中读取状态。
import { takeLatest, call, put, select } from 'redux-saga/effects';
function* fetchUser(action) {
try {
const token = yield select(state => state.auth.token);
const data = yield call(api.getUser, action.payload, token);
yield put({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (err) {
yield put({ type: 'FETCH_USER_ERROR', error: err.message });
}
}
// 监听请求action,自动取消上一次未完成的任务
function* userSaga() {
yield takeLatest('FETCH_USER_REQUEST', fetchUser);
}
export default userSaga;
同样是搜索防抖,在saga里只是几行声明式代码:
import { take, cancel, fork, delay, call, put } from 'redux-saga/effects';
function* watchSearch() {
let lastTask;
while (true) {
const action = yield take('SEARCH_INPUT');
if (lastTask) {
yield cancel(lastTask); // 取消上一个搜索任务
}
lastTask = yield fork(handleSearch, action.payload);
}
}
function* handleSearch(keyword) {
yield delay(300); // 300ms内被取消则直接终止
const result = yield call(api.search, keyword);
yield put({ type: 'SEARCH_SUCCESS', payload: result });
}
saga的强大还体现在并发控制上,all可以并行执行多个任务,race可以实现请求超时竞争,fork启动非阻塞子任务,配合cancel可以做任务取消。这些能力让它在处理轮询、WebSocket长连接、多步骤表单提交等复杂场景时依然保持清晰的流程描述。
当然代价也不小:Generator加一整套API概念的学习曲线陡峭,包体积比thunk大一个量级,而且每个接口都要写action、watcher、worker三层代码,样板代码反而更多。对于接口不多的项目,引入saga有点杀鸡用牛刀。
四、如何选型
两个库的对比可以简单归纳如下:
| 维度 | redux-thunk | redux-saga |
|---|---|---|
| 学习成本 | 极低,十分钟上手 | 较高,需理解Generator |
| 包体积 | 约1KB | 约20KB |
| 流程控制 | 手动处理,能力较弱 | 内置取消、防抖、并发、重试 |
| 代码组织 | 逻辑分散在actionCreator | 副作用集中管理 |
| 可测试性 | 一般 | 好,yield结果可逐一断言 |
实践建议是:新项目或者业务以简单CRUD为主,先用redux-thunk快速落地;当出现大量需要防抖、取消、轮询的复杂交互,或者团队希望副作用有统一归口、便于维护和测试时,再迁移到redux-saga。两者也可以共存,用applyMiddleware(thunk, sagaMiddleware)同时挂载,实现渐进式迁移。
另外提一句,如果项目还在起步阶段,也不妨考虑Redux Toolkit自带的createAsyncThunk,它内置了thunk并自动生成pending、fulfilled、rejected三种action,能省掉不少样板代码,是官方目前更推荐的起点。真正理解了thunk和saga的设计差异,再回头看这些上层封装,会明白它们只是同一套中间件思想的不同产品化形态。
Redux中间件redux-thunkredux-saga修改时间:2026-09-08 07:29:05