Skip to content

Redux的中间件(Middleware)的工作机制

在 Redux 的生态中,Reducer 必须是纯函数,这意味着它只负责根据旧的 State 和 Action 来计算新的 State,不能包含任何副作用,例如网络请求、定时器、或者访问浏览器 API。但在实际的 Web 应用开发中,处理异步操作是不可避免的。比如,用户点击按钮后,我们需要向服务器发起请求,然后根据请求成功或失败的结果来更新状态。

我们常常遇到这样一个场景:一个 Action 被 dispatch 之后,我们希望在它到达 Reducer 之前执行一些额外的逻辑,尤其是异步逻辑。如果直接将这些副作用放在 Reducer 中,就破坏了 Redux 的核心原则;如果将它们分散在各个组件中,又会导致逻辑混乱和难以维护。

为了优雅地解决这个矛盾,Redux 引入了中间件(Middleware)机制。它为我们提供了一个统一、可组合的方式来处理这些副作用,同时保持 Reducer 的纯粹性。

什么是中间件?

我们可以把 Redux 中间件想象成一个位于 Action 发出后、到达 Reducer 前的"关卡"或"扩展点"。当一个 Action 被 dispatch 时,它不会立即到达 Reducer,而是会先穿过一个由所有中间件组成的链条。

我们可以用一个简单的流程图来表示这个过程:

Action -> Middleware 1 -> Middleware 2 -> ... -> Reducer

在这个链条上,每个中间件都可以接触到被派发的 Action,并能访问到 Store 的 getState() 和 dispatch() 方法。这赋予了中间件强大的能力:

  • 执行副作用:比如发起 API 请求。
  • 修改或延迟 Action:可以等待异步操作完成后,再派发一个新的 Action。
  • 中止 Action:在特定条件下,阻止某个 Action 到达 Reducer。
  • 派发新的 Action:在处理一个 Action 的过程中,可以派发其他完全不同的 Action。

本质上,中间件通过增强(monkey-patching)Redux Store 的 dispatch 方法,为我们提供了一个介入数据流处理过程的机会。

中间件的核心签名:store => next => action

所有 Redux 中间件都遵循一个看似有些复杂的函数签名。它是一个三层嵌套的柯里化函数:

javascript
const myMiddleware = store => next => action => {
  // 中间件的逻辑在这里
}

为了更好地理解它,我们把它拆解来看:

  1. store:这是函数的第一层接收的参数。Redux 在应用中间件时,会将 Store 实例传递进来。因此,在中间件内部,我们可以通过 store.getState() 获取当前状态,或通过 store.dispatch() 派发新的 Action。

  2. next:这是第二层函数接收的参数。next 是一个函数,它是中间件链条中的"接力棒"。当我们调用 next(action) 时,就是将这个 Action 传递给链条中的下一个中间件。如果当前中间件是最后一个,那么 next 就是原始的 store.dispatch 方法,调用它会将 Action 直接发送给 Reducer。如果我们不调用 next(action),那么这个 Action 将被"拦截",无法继续传递下去。

  3. action:这是最内层函数接收的参数,也就是当前正在被处理的 Action 对象。

这个精巧的结构使得中间件可以形成一个清晰的洋葱模型。每个中间件都可以决定在调用 next(action) 之前之后做什么。

实践:编写一个简单的日志中间件

理论不如实践,我们来编写一个最经典的日志中间件 logger。它的功能是在每个 Action 到达 Reducer 前后,分别打印出 Action 的内容、变化前的状态和变化后的状态。

javascript
const loggerMiddleware = store => next => action => {
  // 1. Action 到达 Reducer 之前的逻辑
  console.log('Dispatching:', action);
  console.log('State before:', store.getState());

  // 2. 调用 next,将 Action 传递给下一个中间件或 Reducer
  // 这是整个流程中的"分割点"
  const result = next(action);

  // 3. Action 处理完毕后的逻辑
  console.log('State after:', store.getState());

  // 4. 返回 next(action) 的结果
  return result;
};

这个例子清晰地展示了中间件的工作模式:

  • 代码在 next(action) 之前执行,这是 Action 到达 Reducer 之前的阶段。
  • next(action) 是一个关键的调用,它触发了后续的中间件以及最终的 Reducer。
  • 代码在 next(action) 之后执行,这时 Reducer 已经完成了状态更新,我们可以获取到最新的 State。

处理异步请求:中间件的真正威力

现在,我们来解决文章开头提到的异步请求问题。最常见的方式是使用像 redux-thunk 这样的中间件。Thunk 的核心思想很简单:让 Action Creator 不仅能返回一个 Action 对象,还能返回一个函数。

我们可以自己实现一个简化的 thunk 中间件来理解其原理:

javascript
const thunkMiddleware = store => next => action => {
  // 如果 action 不是一个函数,那么这个中间件什么也不做,
  // 直接把它传递给下一个中间件或 Reducer。
  if (typeof action !== 'function') {
    return next(action);
  }

  // 如果 action 是一个函数,我们就执行它,
  // 并将 store 的 dispatch 和 getState 方法作为参数传进去。
  // 这样,在异步逻辑内部,我们就可以自由地派发新的 Action 或获取当前状态。
  return action(store.dispatch, store.getState);
};

有了这个中间件,我们就可以这样编写异步 Action 了:

javascript
// 这是一个 Action Creator,但它返回了一个函数,而不是 Action 对象
const fetchUserData = () => {
  // 这个函数会被 thunkMiddleware 执行
  return async (dispatch, getState) => {
    // 派发一个 "请求开始" 的 Action
    dispatch({ type: 'FETCH_USER_REQUEST' });

    try {
      // 执行异步操作
      const response = await fetch('/api/user');
      const user = await response.json();

      // 异步成功后,派发 "请求成功" 的 Action,并附带数据
      dispatch({ type: 'FETCH_USER_SUCCESS', payload: user });
    } catch (error) {
      // 异步失败后,派发 "请求失败" 的 Action,并附带错误信息
      dispatch({ type: 'FETCH_USER_FAILURE', payload: error.message });
    }
  };
};

// 在组件中这样使用:
// store.dispatch(fetchUserData());

通过这种方式,thunkMiddleware 拦截了函数形式的 action,并执行它,从而将异步流程控制权交给了我们。而同步的 Action 对象则被忽略,继续沿着中间件链条传递,最终由 Reducer 处理。整个过程保持了 Reducer 的纯粹性。

这一切是如何组合起来的:applyMiddleware

我们创建好了中间件,那么 Redux 是如何加载并使用它们的呢?答案是 createStore 函数的第二个(或第三个)参数 applyMiddleware。

applyMiddleware 是 Redux 提供的一个高阶函数,它接收任意数量的中间件作为参数,并返回一个 Store Enhancer (一个增强器函数)。

javascript
import { createStore, applyMiddleware } from 'redux';
import rootReducer from './reducers';
import loggerMiddleware from './middlewares/logger';
import thunkMiddleware from './middlewares/thunk';

// 使用 applyMiddleware 将所有中间件组合起来
const store = createStore(
  rootReducer,
  applyMiddleware(loggerMiddleware, thunkMiddleware)
);

applyMiddleware 的内部机制大致是这样的:

  1. 它获取 store.dispatch 的原始版本。
  2. 将中间件数组通过函数式编程中的 compose 方法,从右到左组合成一个调用链。例如 applyMiddleware(m1, m2, m3) 会变成 m1(m2(m3(...))) 的结构。
  3. 它将 store.dispatch 作为这个组合链条的最终 next 函数,然后生成一个全新的、被所有中间件包裹起来的"增强版" dispatch 函数。
  4. 最后,用这个新的 dispatch 替换掉原始的 store.dispatch。

之后,每当我们调用 store.dispatch(action) 时,实际上触发的是经过层层包裹的中间件链条。

总结

Redux 中间件是一个强大而灵活的机制,它优雅地解决了在函数式数据流中处理副作用的难题。我们可以总结出它的几个核心特点:

  • 核心目的:处理副作用(如异步请求),保持 Reducer 的纯粹性。
  • 工作位置:位于 dispatch 和 Reducer 之间,形成一个可插拔的处理链。
  • 核心签名:store => next => action 的柯里化结构,使其易于组合和链式调用。
  • 关键函数:next(action) 是驱动 Action 在中间件链条中传递的"引擎"。
  • 集成方式:通过 applyMiddleware 函数应用到 Store 中,它会生成一个被增强的 dispatch 方法。

它成功地将应用的业务逻辑(如数据获取、缓存、日志等)从视图层和纯粹的状态管理中剥离出来,让我们的代码结构更加清晰、可预测和易于维护。希望通过这篇文章,我们能够对 Redux 中间件的机制有一个更清晰、更深入的理解。

基于 VitePress 的本地知识库