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

我们创建好了中间件,那么 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

总结

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

  • 核心目的:处理副作用(如异步请求),保持 Reducer 的纯粹性。

  • 工作位置:位于 dispatch 和 Reducer 之间,形成一个可插拔的处理链。

  • 核心签名store => next => action 的柯里化结构,使其易于组合和链式调用。

  • 关键函数next(action) 是驱动 Action 在中间件链条中传递的“引擎”。

  • 集成方式:通过 applyMiddleware 函数应用到 Store 中,它会生成一个被增强的 dispatch 方法。

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

不知道说啥了很无语了