Redux 工作流与中间件
Redux 的 Store、Action、Reducer 工作流程及 Thunk、Saga 中间件原理
问题
解释 Redux 的工作流(Store, Action, Reducer)以及中间件(Thunk, Saga)的原理。
解答
Redux 工作流
┌─────────────────────────────────────────────────┐
│ │
│ dispatch(action) │
│ │ │
│ ▼ │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ │
│ │ Action │───▶│ Reducer │───▶│ Store │ │
│ └─────────┘ └─────────┘ └─────────┘ │
│ │ │
│ ▼ │
│ ┌──────────┐ │
│ │ View │ │
│ └──────────┘ │
│ │ │
│ dispatch(action) ◀─┘ │
└─────────────────────────────────────────────────┘
基本实现
// 创建 Store
function createStore(reducer, preloadedState, enhancer) {
// 如果有 enhancer(中间件),先应用 enhancer
if (typeof enhancer === 'function') {
return enhancer(createStore)(reducer, preloadedState);
}
let state = preloadedState;
let listeners = [];
// 获取当前状态
function getState() {
return state;
}
// 派发 action,触发 reducer 更新状态
function dispatch(action) {
state = reducer(state, action);
// 通知所有订阅者
listeners.forEach(listener => listener());
return action;
}
// 订阅状态变化
function subscribe(listener) {
listeners.push(listener);
// 返回取消订阅函数
return () => {
listeners = listeners.filter(l => l !== listener);
};
}
// 初始化状态
dispatch({ type: '@@INIT' });
return { getState, dispatch, subscribe };
}
// Reducer 示例
function counterReducer(state = { count: 0 }, action) {
switch (action.type) {
case 'INCREMENT':
return { count: state.count + 1 };
case 'DECREMENT':
return { count: state.count - 1 };
default:
return state;
}
}
// 使用
const store = createStore(counterReducer);
store.subscribe(() => console.log(store.getState()));
store.dispatch({ type: 'INCREMENT' }); // { count: 1 }
中间件原理
中间件通过包装 dispatch 方法,在 action 到达 reducer 之前进行拦截处理。
// applyMiddleware 实现
function applyMiddleware(...middlewares) {
return (createStore) => (reducer, preloadedState) => {
const store = createStore(reducer, preloadedState);
let dispatch = store.dispatch;
// 传给中间件的 API
const middlewareAPI = {
getState: store.getState,
dispatch: (action) => dispatch(action)
};
// 执行中间件,获取包装函数
const chain = middlewares.map(middleware => middleware(middlewareAPI));
// 组合中间件,包装 dispatch
dispatch = compose(...chain)(store.dispatch);
return { ...store, dispatch };
};
}
// compose 函数:从右到左组合函数
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)));
}
Redux Thunk
Thunk 让 dispatch 可以接收函数,用于处理异步操作。
// thunk 中间件实现
function thunk({ dispatch, getState }) {
return (next) => (action) => {
// 如果 action 是函数,执行它并传入 dispatch 和 getState
if (typeof action === 'function') {
return action(dispatch, getState);
}
// 否则继续传递给下一个中间件
return next(action);
};
}
// 使用 thunk 处理异步
const fetchUser = (id) => async (dispatch, getState) => {
dispatch({ type: 'FETCH_USER_START' });
try {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
} catch (error) {
dispatch({ type: 'FETCH_USER_ERROR', payload: error.message });
}
};
// 派发异步 action
store.dispatch(fetchUser(1));
Redux Saga
Saga 使用 Generator 函数管理副作用,更适合复杂的异步流程。
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects';
// Worker Saga:执行异步任务
function* fetchUserSaga(action) {
try {
// call:调用异步函数
const user = yield call(fetch, `/api/users/${action.payload}`);
const data = yield call([user, 'json']);
// put:派发 action
yield put({ type: 'FETCH_USER_SUCCESS', payload: data });
} catch (error) {
yield put({ type: 'FETCH_USER_ERROR', payload: error.message });
}
}
// Watcher Saga:监听 action
function* watchFetchUser() {
// takeEvery:每次都执行
yield takeEvery('FETCH_USER_REQUEST', fetchUserSaga);
// takeLatest:只执行最新的,取消之前的
// yield takeLatest('FETCH_USER_REQUEST', fetchUserSaga);
}
// Root Saga
function* rootSaga() {
yield all([
watchFetchUser(),
// 其他 watcher...
]);
}
// 简化版 Saga 中间件原理
function createSagaMiddleware() {
let boundRunSaga;
const middleware = ({ dispatch, getState }) => {
boundRunSaga = (saga) => {
const iterator = saga();
function next(value) {
const result = iterator.next(value);
if (result.done) return;
const effect = result.value;
// 处理不同的 effect
if (effect.type === 'CALL') {
effect.fn(...effect.args).then(next);
} else if (effect.type === 'PUT') {
dispatch(effect.action);
next();
}
}
next();
};
return (next) => (action) => next(action);
};
middleware.run = (saga) => boundRunSaga(saga);
return middleware;
}
Thunk vs Saga 对比
| 特性 | Thunk | Saga |
|---|---|---|
| 学习成本 | 低 | 高(需要了解 Generator) |
| 代码量 | 少 | 多 |
| 测试 | 较难(需要 mock) | 简单(纯函数) |
| 复杂流程 | 不擅长 | 擅长(竞态、取消、重试) |
| 适用场景 | 简单异步 | 复杂异步流程 |
关键点
- 单向数据流:View → Action → Reducer → Store → View
- 中间件本质:对 dispatch 的增强,形成洋葱模型
- Thunk:让 dispatch 支持函数,函数内可执行异步操作
- Saga:用 Generator 声明式描述副作用,便于测试和控制复杂流程
- 选择建议:简单项目用 Thunk,复杂异步流程用 Saga
目录