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 对比

特性ThunkSaga
学习成本高(需要了解 Generator)
代码量
测试较难(需要 mock)简单(纯函数)
复杂流程不擅长擅长(竞态、取消、重试)
适用场景简单异步复杂异步流程

关键点

  • 单向数据流:View → Action → Reducer → Store → View
  • 中间件本质:对 dispatch 的增强,形成洋葱模型
  • Thunk:让 dispatch 支持函数,函数内可执行异步操作
  • Saga:用 Generator 声明式描述副作用,便于测试和控制复杂流程
  • 选择建议:简单项目用 Thunk,复杂异步流程用 Saga