redux-thunk、redux-saga 和 redux-observable 都是用于管理 Redux 应用中异步逻辑和副作用的中间件方案。redux-thunk 允许 action creator 返回函数而不是普通对象,是最简单直接的方案,已内置于 Redux Toolkit。redux-saga 使用 ES6 生成器函数(Generators)来处理异步流程,擅长处理复杂的竞态条件和取消操作。redux-observable 基于 RxJS,将 action 视为数据流,适合处理高度复杂的事件交互和流式数据转换。三者各有适用场景,选择取决于项目的复杂度团队的技术栈偏好。
在 Redux 生态系统中,处理异步逻辑(如 API 请求、WebSocket 连接)是核心挑战之一。redux-thunk、redux-saga 和 redux-observable 提供了三种不同的解决思路。本文将从实现方式、复杂流程控制、取消机制和测试难度四个维度进行深度对比。
redux-thunk 允许 action creator 返回一个函数,该函数接收 dispatch 和 getState 作为参数。
// redux-thunk
const fetchUser = (id) => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER_REQUEST' });
try {
const user = await api.getUser(id);
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_FAILURE', error });
}
};
redux-saga 使用 ES6 生成器函数(function*)和 yield 表达式。
call、put 等效果指令描述意图。// redux-saga
import { call, put, takeEvery } from 'redux-saga/effects';
function* fetchUser(action) {
try {
const user = yield call(api.getUser, action.payload);
yield put({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
yield put({ type: 'FETCH_USER_FAILURE', error });
}
}
yield takeEvery('FETCH_USER_REQUEST', fetchUser);
redux-observable 基于 RxJS,Epics 是接收 action 流并返回 action 流的函数。
mergeMap、map)转换数据流。// redux-observable
import { ofType } from 'redux-observable';
import { mergeMap, map, catchError } from 'rxjs/operators';
const fetchUserEpic = (action$, state$) =>
action$.pipe(
ofType('FETCH_USER_REQUEST'),
mergeMap(action =>
api.getUser(action.payload).pipe(
map(user => ({ type: 'FETCH_USER_SUCCESS', payload: user })),
catchError(error => of({ type: 'FETCH_USER_FAILURE', error }))
)
)
);
redux-thunk 依赖原生 async/await 或 Promise.all。
// redux-thunk
const fetchDashboard = () => async (dispatch) => {
const [users, posts] = await Promise.all([
api.getUsers(),
api.getPosts()
]);
dispatch({ type: 'DASHBOARD_LOADED', payload: { users, posts } });
};
redux-saga 提供内置的效果指令来处理并发和顺序。
all 用于并行,sequence 用于顺序。// redux-saga
import { all, call, put } from 'redux-saga/effects';
function* fetchDashboard() {
const [users, posts] = yield all([
call(api.getUsers),
call(api.getPosts)
]);
yield put({ type: 'DASHBOARD_LOADED', payload: { users, posts } });
}
redux-observable 使用 RxJS 操作符组合流。
forkJoin 类似 Promise.all。concatMap 保证顺序执行。// redux-observable
import { forkJoin, of } from 'rxjs';
import { mergeMap, map } from 'rxjs/operators';
const fetchDashboardEpic = (action$) =>
action$.pipe(
ofType('FETCH_DASHBOARD'),
mergeMap(() =>
forkJoin({
users: api.getUsers(),
posts: api.getPosts()
}).pipe(
map(results => ({ type: 'DASHBOARD_LOADED', payload: results }))
)
)
);
redux-thunk 需要手动使用 AbortController 或标记位。
// redux-thunk
let abortController;
const fetchUser = (id) => async (dispatch) => {
if (abortController) abortController.abort();
abortController = new AbortController();
try {
const user = await api.getUser(id, { signal: abortController.signal });
dispatch({ type: 'SUCCESS', payload: user });
} catch (e) {
if (e.name !== 'AbortError') throw e;
}
};
redux-saga 内置强大的取消支持。
takeLatest 自动取消之前的任务。cancelled 判断是否被取消。// redux-saga
import { takeLatest, cancelled, call } from 'redux-saga/effects';
function* fetchUser(action) {
try {
const user = yield call(api.getUser, action.payload);
yield put({ type: 'SUCCESS', payload: user });
} finally {
if (yield cancelled()) {
// 清理逻辑
}
}
}
yield takeLatest('FETCH_USER_REQUEST', fetchUser);
redux-observable 通过操作符自然处理取消。
switchMap 自动取消内部未完成的 Observable。takeUntil 监听特定 action 来取消。// redux-observable
import { switchMap, takeUntil } from 'rxjs/operators';
const fetchUserEpic = (action$) =>
action$.pipe(
ofType('FETCH_USER_REQUEST'),
switchMap(action =>
api.getUser(action.payload).pipe(
takeUntil(action$.pipe(ofType('CANCEL_FETCH_USER')))
)
)
);
redux-thunk 测试最简单。
dispatch。// redux-thunk test
const dispatch = jest.fn();
await fetchUser(1)(dispatch, () => ({}));
expect(dispatch).toHaveBeenCalledWith({ type: 'SUCCESS' });
redux-saga 需要遍历生成器。
yield 点。// redux-saga test
const iterator = fetchUser({ payload: 1 });
expect(iterator.next().value).toEqual(call(api.getUser, 1));
expect(iterator.next({ id: 1 }).value).toEqual(put({ type: 'SUCCESS' }));
redux-observable 需要测试 Observable 流。
// redux-observable test
const action$ = of({ type: 'FETCH_USER_REQUEST', payload: 1 });
const epic$ = fetchUserEpic(action$);
epic$.subscribe(action => {
expect(action.type).toBe('SUCCESS');
});
| 特性 | redux-thunk | redux-saga | redux-observable |
|---|---|---|---|
| 核心概念 | 函数返回函数 | 生成器 (Generators) | 响应式流 (RxJS) |
| 学习曲线 | 低 | 中 | 高 |
| 异步风格 | async/await | 同步式 yield | 流式操作符 |
| 取消支持 | 手动 (AbortController) | 内置 (takeLatest) | 内置 (switchMap) |
| 测试难度 | 简单 | 中等 | 复杂 |
| Redux Toolkit | 内置支持 | 需手动配置中间件 | 需手动配置中间件 |
redux-thunk 是大多数项目的首选 🥇。特别是配合 Redux Toolkit 使用时,createAsyncThunk 进一步简化了样板代码。如果你的团队不熟悉生成器或响应式编程,不要过度设计。
redux-saga 适合复杂业务场景 🏢。当你需要处理复杂的取消逻辑、竞态条件或后台任务时,Saga 的结构化模型非常可靠。它在大型企业中依然有广泛使用。
redux-observable 适合流式数据场景 🌊。如果你的应用重度依赖 WebSocket、实时事件或复杂的用户交互流,RxJS 的组合能力是无与伦比的。但请确保团队有能力维护 RxJS 代码。
核心原则:不要为了技术而技术。从 redux-thunk 开始,只有在遇到无法优雅解决的复杂异步问题时,再考虑引入 Saga 或 Observable。
选择 redux-saga 如果你需要处理复杂的异步流程控制,例如取消正在进行的请求、处理竞态条件或执行后台轮询。生成器函数让异步代码看起来像同步代码,便于阅读和调试。适合对流程控制有严格要求的大型企业级应用。
选择 redux-observable 如果你的应用涉及复杂的事件流交互,例如防抖、节流、多个事件源合并或取消。基于 RxJS 的响应式编程模型非常适合处理实时数据流。适合团队已经熟悉 RxJS 或需要极高灵活性的场景。
选择 redux-thunk 如果你的项目逻辑相对简单,或者你正在使用 Redux Toolkit(已内置)。它学习曲线最低,代码最直观,适合大多数常规的数据获取和表单提交场景。对于不需要复杂取消、竞态处理或事件流组合的项目,这是最务实的选择。
See our website for more information.