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

一、Redux 中间件是什么?为什么需要 Redux 中间件?

面试题 1:什么是 Redux 中间件?为什么需要 Redux 中间件?

核心思路(一句话)

Redux 中间件本质上是对 dispatch 的一层可组合包装,让 Action 在进入 Reducer 之前可以执行日志、异步请求、权限校验、错误处理等额外逻辑。


1. 先理解没有中间件时 Redux 怎么工作

Redux 最核心的流程非常简单:

text 复制代码
dispatch(action)
      ↓
Reducer(state, action)
      ↓
新的 state
      ↓
通知订阅者

例如:

javascript 复制代码
store.dispatch({
  type: 'counter/increment',
  payload: 1
});

最终会进入:

javascript 复制代码
reducer(previousState, action);

Reducer 的职责应该非常单一:

text 复制代码
旧 state + Action
      ↓
纯计算
      ↓
新 state

它不应该负责:

text 复制代码
网络请求
定时器
日志
本地存储
路由跳转
权限判断
埋点
异常上报

这些属于副作用或者横切逻辑。


二、为什么需要中间件?

主要矛盾

Redux 的核心状态更新流程要求简单、可预测,而真实应用又存在大量副作用和横切逻辑。

次要矛盾

如果把这些逻辑全部塞进组件或者 Reducer:

text 复制代码
组件
 ├── 请求 API
 ├── 判断权限
 ├── 日志
 ├── 错误处理
 ├── 埋点
 └── dispatch

最终会导致:

  • 逻辑重复
  • 组件职责膨胀
  • Reducer 不再纯粹
  • 异步流程难以复用
  • 测试困难
  • 横切逻辑难以统一管理

所以 Redux 提供了 Middleware。


三、Redux 中间件的位置

这里要纠正一个容易产生误解的地方。

不能简单理解为:

text 复制代码
dispatch → middleware → reducer

更准确的理解是:

Middleware 包装了 Redux 的 dispatch,形成一个 Action 处理链。

完整流程:

text 复制代码
store.dispatch(action)
        ↓
┌─────────────────────┐
│ Middleware 1        │
│                   │
│   next(action)     │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ Middleware 2        │
│                   │
│   next(action)     │
└─────────┬───────────┘
          ↓
┌─────────────────────┐
│ Middleware 3        │
└─────────┬───────────┘
          ↓
      原始 dispatch
          ↓
       Reducer
          ↓
      更新 Store

但是中间件还可以在 next(action) 之后继续执行代码:

text 复制代码
dispatch
   ↓
Middleware A:前置逻辑
   ↓
Middleware B:前置逻辑
   ↓
Reducer
   ↓
Middleware B:后置逻辑
   ↓
Middleware A:后置逻辑
   ↓
dispatch 返回

所以它实际上具有一种类似"洋葱模型"的结构。


四、Redux 中间件到底解决了什么问题?

可以归纳成 4 类。

① 副作用处理

例如:

text 复制代码
网络请求
定时器
WebSocket
本地存储
日志
埋点
路由跳转

② 扩展 dispatch 能力

Redux 原生的基础 dispatch 主要处理普通 Action 对象:

javascript 复制代码
dispatch({
  type: 'user/loginSuccess',
  payload: user
});

通过 Middleware,可以进一步支持:

javascript 复制代码
dispatch(function (dispatch, getState) {
  // 异步逻辑
});

这就是 Redux Thunk 的基本思想。


③ 横切逻辑复用

例如统一处理:

text 复制代码
所有 Action 日志
所有请求错误
所有权限校验
所有埋点
所有性能监控

不需要每个组件重复写。


④ 多个中间件组合

例如:

text 复制代码
Thunk
  +
Logger
  +
Error Reporting
  +
Analytics

共同组成一个 Middleware Pipeline。


五、Redux Middleware 的核心函数签名

面试题 2:Redux 中间件的函数签名是什么?每个参数有什么作用?

核心思路(一句话)

Redux Middleware 是一个三层函数结构:第一层拿到 Store API,第二层拿到下游处理函数,第三层拿到当前 Action。

标准形式:

javascript 复制代码
const middleware = ({ dispatch, getState }) => next => action => {
  // 中间件逻辑
};

也可以完整展开:

javascript 复制代码
const middleware = function (store) {
  return function (next) {
    return function (action) {
      // middleware logic
    };
  };
};

六、三个参数分别是什么?

第一层:store

javascript 复制代码
({ dispatch, getState })

这里拿到的是 Store API。

最重要的是:

javascript 复制代码
store.dispatch
store.getState

getState

获取当前 Redux State:

javascript 复制代码
const state = getState();

dispatch

重新派发一个 Action:

javascript 复制代码
dispatch({
  type: 'user/loginSuccess'
});

注意:

这里的 dispatch 是经过 Middleware 增强后的 dispatch。

因此它重新派发的 Action 会重新进入整个 Middleware 链。


七、第二层:next

javascript 复制代码
next(action);

它表示:

把当前 Action 交给当前 Middleware 的下游。

注意这里是一个非常重要的面试点。

如果有:

text 复制代码
Middleware A
Middleware B
Middleware C
Reducer

那么:

javascript 复制代码
Middleware A → next(action)

会进入:

text 复制代码
Middleware B

而:

javascript 复制代码
Middleware B → next(action)

会进入:

text 复制代码
Middleware C

最后一个 Middleware:

javascript 复制代码
next(action)

最终会进入 Redux 原始的 dispatch:

text 复制代码
原始 dispatch
      ↓
Reducer

八、第三层:action

javascript 复制代码
action => {}

就是当前正在处理的 Action。

例如:

javascript 复制代码
{
  type: 'user/login',
  payload: {
    username: 'zhangsan'
  }
}

九、为什么 Redux Middleware 要设计成三层函数?

这是面试官非常喜欢追问的原理。

因为 Redux 在创建 Middleware 链的时候,需要:

text 复制代码
第一步:给 Middleware Store API
              ↓
第二步:给 Middleware 下一个处理节点
              ↓
第三步:让 Middleware 处理具体 Action

所以形成:

javascript 复制代码
({ dispatch, getState }) => next => action => {}

它实际上是一个函数式的责任链设计。


十、Middleware 最核心的原理:重新包装 dispatch

面试题 3:Redux 的 Middleware 到底是怎么实现的?

核心思路(一句话)

applyMiddleware 本质上就是把多个 Middleware 组合成一个函数链,再用这个函数链重新包装 Store 原本的 dispatch。

这是整个 Redux Middleware 最核心的原理。


十一、没有 Middleware 时

假设 Redux 原始:

javascript 复制代码
const store = createStore(reducer);

它的:

javascript 复制代码
store.dispatch

最终可以抽象成:

text 复制代码
dispatch(action)
       ↓
Reducer
       ↓
更新 state

十二、加入一个 Middleware

假设:

javascript 复制代码
const logger = ({ dispatch, getState }) => next => action => {
  console.log(action);

  return next(action);
};

那么实际上变成:

text 复制代码
store.dispatch
      ↓
logger
      ↓
原始 dispatch
      ↓
Reducer

也就是说:

javascript 复制代码
store.dispatch

已经不是原始的 dispatch 了。

而是 Middleware 包装之后的新 dispatch。


十三、多个 Middleware 是怎么组合的?

假设:

javascript 复制代码
applyMiddleware(
  middlewareA,
  middlewareB,
  middlewareC
)

逻辑上可以理解成:

text 复制代码
dispatch
  ↓
middlewareA
  ↓
middlewareB
  ↓
middlewareC
  ↓
原始 dispatch

可以抽象成:

javascript 复制代码
dispatch =
  middlewareA(
    middlewareB(
      middlewareC(
        originalDispatch
      )
    )
  );

更准确地说,是每个 Middleware 接收到一个 next,最终形成一个嵌套调用链。


十四、为什么 next(action) 能把 Action 传给下一个 Middleware?

因为 Redux 在构建 Middleware 链的时候,已经把:

javascript 复制代码
next

绑定到了下一个处理节点。

可以把它想象成:

text 复制代码
Middleware A
      │
      │ next
      ↓
Middleware B
      │
      │ next
      ↓
Middleware C
      │
      │ next
      ↓
Original Dispatch

所以:

javascript 复制代码
next(action);

并不是某种神奇的 Redux API。

它本质上就是当前函数闭包中保存的"下一个处理节点"。


十五、自己实现一个极简版 Middleware

这个例子非常适合面试时讲底层原理。

javascript 复制代码
/**
 * 一个极简版的 Middleware 组合器。
 *
 * 注意:
 * 这里只是为了理解 Redux Middleware 的核心思想,
 * 不是完整复制 Redux 官方实现。
 */
function applyMiddlewareSimple(...middlewares) {
  return function enhanceStore(createStore) {
    return function createEnhancedStore(reducer, preloadedState) {
      // 第一步:先创建原始 Store。
      const store = createStore(reducer, preloadedState);

      // 第二步:先保存原始 dispatch。
      // 因为后面我们会用 Middleware 重新包装 dispatch。
      let dispatch = store.dispatch;

      // 第三步:把 Store API 传给每一个 Middleware。
      //
      // 注意这里不能直接把完整 store 传进去,
      // 实际上 Redux Middleware 主要依赖 dispatch 和 getState。
      const middlewareAPI = {
        getState: store.getState,

        // 这里使用动态引用 dispatch。
        // 这样 Middleware 内部调用 dispatch 时,
        // 最终拿到的是增强后的 dispatch。
        dispatch: (action) => dispatch(action)
      };

      // 第四步:
      // 每一个 Middleware 执行第一层函数,
      // 得到一个等待接收 next 的函数。
      const chain = middlewares.map(
        middleware => middleware(middlewareAPI)
      );

      // 第五步:
      // 把 Middleware 链组合起来。
      //
      // 最终得到:
      //
      // middlewareA(
      //   middlewareB(
      //     middlewareC(
      //       originalDispatch
      //     )
      //   )
      // )
      //
      // reduceRight 的目的就是从最后一个 Middleware 开始,
      // 一层一层向前包装。
      dispatch = chain.reduceRight(
        (next, middleware) => middleware(next),
        dispatch
      );

      // 第六步:
      // 返回增强后的 Store。
      return {
        ...store,
        dispatch
      };
    };
  };
}

这里最值得记住的只有一句:

javascript 复制代码
dispatch = chain.reduceRight(
  (next, middleware) => middleware(next),
  dispatch
);

它表达的就是:

text 复制代码
多个 Middleware
      ↓
组合
      ↓
重新包装原始 dispatch

十六、Redux 日志 Middleware 怎么实现?

面试题 4:如何手写一个 Redux Logger Middleware?

核心思路(一句话)

在 next(action) 前读取旧状态,在 next(action) 后读取新状态。

javascript 复制代码
/**
 * Redux Logger Middleware
 *
 * 功能:
 * 1. 打印当前 Action
 * 2. 打印 Action 执行前的 State
 * 3. 调用 next,让 Action 继续执行
 * 4. 打印 Action 执行后的 State
 */
const loggerMiddleware = ({ getState }) => next => action => {
  // 获取 Action 执行前的 State。
  const previousState = getState();

  console.log('========== Redux Action ==========');

  console.log('Action:', action);

  console.log('Previous State:', previousState);

  // 非常关键:
  // 必须调用 next,否则 Action 不会继续向下传递。
  //
  // 如果当前不是最后一个 Middleware,
  // 它会进入下一个 Middleware。
  //
  // 如果当前已经是最后一个 Middleware,
  // 它最终会进入 Redux 原始 dispatch,
  // 然后触发 Reducer。
  const result = next(action);

  // next 返回之后,说明下游处理已经完成。
  // 此时再次获取 State,就可以拿到更新后的 State。
  const nextState = getState();

  console.log('Next State:', nextState);

  console.log('=================================');

  // 返回 next 的结果。
  // 这样可以保持整个 Middleware 链的返回值传递。
  return result;
};

十七、为什么要把 next(action) 的返回值返回出去?

这是一个容易被忽略的细节。

例如:

javascript 复制代码
const result = next(action);

return result;

因为 Redux Middleware 本身可以改变 dispatch() 的返回值。

例如:

javascript 复制代码
const result = store.dispatch(action);

这个 result 最终可能来自:

text 复制代码
Middleware A
    ↓
Middleware B
    ↓
Middleware C
    ↓
原始 dispatch

如果某个 Middleware 不返回:

javascript 复制代码
return next(action);

就可能导致:

javascript 复制代码
store.dispatch(action)

得到:

javascript 复制代码
undefined

所以规范写法通常是:

javascript 复制代码
return next(action);

十八、next(action) 和 store.dispatch(action) 有什么区别?

面试题 5:Redux Middleware 中 next(action) 和 dispatch(action) 有什么区别?

核心思路(一句话)

next 是沿当前 Middleware 链继续向后传递;dispatch 是重新从整个 Middleware 链的起点派发一个 Action。

这是 Redux Middleware 最重要的面试题之一。


情况一:调用 next(action)

javascript 复制代码
next(action);

假设当前处于 Middleware B:

text 复制代码
A
 ↓
B ← 当前
 ↓
C
 ↓
Reducer

调用:

javascript 复制代码
next(action);

会变成:

text 复制代码
B
 ↓
C
 ↓
Reducer

不会重新经过 A、B。


十九、调用 dispatch(action) 呢?

如果:

javascript 复制代码
dispatch(action);

那么:

text 复制代码
A
 ↓
B
 ↓
C
 ↓
Reducer

会从 Middleware 链的起点重新开始。

也就是:

text 复制代码
store.dispatch(action)
       ↓
Middleware A
       ↓
Middleware B
       ↓
Middleware C
       ↓
Reducer

二十、用图理解两者

假设:

text 复制代码
A → B → C → Reducer

当前正在 B:

next(action)

text 复制代码
A → B → C → Reducer
          ↑
        从这里继续

dispatch(action)

text 复制代码
A → B → C → Reducer
↑
从头重新开始

所以:

javascript 复制代码
next(action)

与:

javascript 复制代码
dispatch(action)

绝对不是一回事。


二十一、为什么 Redux Thunk 可以处理函数 Action?

面试题 6:Redux Thunk 的原理是什么?为什么 dispatch 可以接收函数?

核心思路(一句话)

Thunk Middleware 判断 Action 是不是函数;如果是函数,就执行它并注入 dispatch 与 getState;如果不是,就继续调用 next(action)。

Redux 原生:

javascript 复制代码
dispatch({
  type: 'user/login'
});

Thunk 之后:

javascript 复制代码
dispatch(async function (dispatch, getState) {
  // 异步逻辑
});

核心代码非常简单:

javascript 复制代码
/**
 * 一个简化版 Redux Thunk Middleware。
 *
 * 它允许:
 *
 * dispatch({
 *   type: 'xxx'
 * });
 *
 * 也允许:
 *
 * dispatch(function (dispatch, getState) {
 *   // 异步逻辑
 * });
 */
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
  // 如果 Action 是函数,
  // 说明这是一个 Thunk。
  if (typeof action === 'function') {
    // 执行这个函数。
    //
    // 把增强后的 dispatch 传进去,
    // 这样异步请求结束之后,
    // 就可以再次派发普通 Action。
    //
    // 同时传入 getState,
    // 让 Thunk 可以读取当前 Redux State。
    return action(dispatch, getState);
  }

  // 如果不是函数,
  // 就说明它是普通 Action。
  //
  // 继续交给下一个 Middleware。
  return next(action);
};

二十二、Redux Thunk 的完整使用场景

例如:

javascript 复制代码
/**
 * 模拟一个异步请求。
 */
function fetchUserApi() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve({
        id: 1,
        name: '张三'
      });
    }, 1000);
  });
}

/**
 * 一个 Thunk Action Creator。
 *
 * 注意:
 * 它返回的不是普通 Action 对象,
 * 而是一个函数。
 */
function fetchUser() {
  return async function (dispatch, getState) {
    // 请求开始之前,告诉 Redux:
    // 用户数据正在加载。
    dispatch({
      type: 'user/fetchStart'
    });

    try {
      // 发起异步请求。
      const user = await fetchUserApi();

      // 请求成功后,
      // 再派发普通 Action。
      dispatch({
        type: 'user/fetchSuccess',
        payload: user
      });
    } catch (error) {
      // 请求失败后,
      // 派发错误 Action。
      dispatch({
        type: 'user/fetchFailure',
        error: String(error)
      });
    }
  };
}

调用:

javascript 复制代码
store.dispatch(fetchUser());

执行过程:

text 复制代码
store.dispatch(fetchUser())
              ↓
        Middleware
              ↓
      判断 Action 类型
              ↓
        是 function
              ↓
执行 function(dispatch, getState)
              ↓
        发起 API 请求
              ↓
        请求成功
              ↓
dispatch({
  type: 'user/fetchSuccess'
})
              ↓
重新进入 Middleware 链
              ↓
           Reducer
              ↓
        更新 Redux State

二十三、一个非常重要的概念:异步到底在哪里?

很多初学者会说:

"Redux Middleware 让 Redux 变成异步了。"

这句话不够准确。

真正发生的是:

text 复制代码
Middleware 可以执行异步逻辑

但是:

text 复制代码
Reducer 仍然是同步计算

例如:

text 复制代码
Thunk Middleware
      ↓
发起异步请求
      ↓
Promise 完成
      ↓
dispatch 普通 Action
      ↓
Reducer 同步更新 State

所以:

Middleware 不是把 Reducer 变成异步,而是把副作用从 Reducer 外置,并在合适的时机重新 dispatch 普通 Action。


二十四、Redux Middleware 为什么不会破坏 Reducer 的纯函数特性?

因为职责被拆开了:

text 复制代码
Middleware
    ↓
处理副作用
    ↓
产生普通 Action
    ↓
Reducer
    ↓
纯函数计算
    ↓
新的 State

例如:

javascript 复制代码
// Middleware / Thunk
const fetchUser = async dispatch => {
  const response = await fetch('/api/user');

  const user = await response.json();

  dispatch({
    type: 'user/fetchSuccess',
    payload: user
  });
};

Reducer:

javascript 复制代码
function userReducer(state = initialState, action) {
  switch (action.type) {
    case 'user/fetchSuccess':
      return {
        ...state,
        user: action.payload
      };

    default:
      return state;
  }
}

Reducer 不关心:

text 复制代码
HTTP
Promise
网络
定时器
日志
随机数

它只关心:

text 复制代码
state + action → nextState

二十五、Middleware 可以阻止 Action 吗?

可以。

例如权限 Middleware:

javascript 复制代码
/**
 * 权限 Middleware 示例。
 *
 * 假设:
 * 只有管理员才能执行 DELETE_USER。
 */
const permissionMiddleware = ({ getState }) => next => action => {
  const state = getState();

  // 判断当前 Action 是否需要管理员权限。
  if (action.type === 'user/delete') {
    const isAdmin = state.user?.role === 'admin';

    // 如果没有权限,
    // 不调用 next。
    //
    // 这意味着 Action 不会继续向下传递,
    // Reducer 也不会收到这个 Action。
    if (!isAdmin) {
      console.warn('没有删除用户的权限');

      return {
        blocked: true
      };
    }
  }

  // 有权限,正常继续。
  return next(action);
};

流程:

text 复制代码
dispatch(action)
      ↓
权限 Middleware
      ↓
没有权限?
   ↙       ↘
 是         否
 ↓          ↓
阻止       next
 ↓          ↓
结束       Reducer

二十六、Middleware 可以延迟 Action 吗?

可以。

例如:

javascript 复制代码
const delayMiddleware = () => next => action => {
  // 只延迟指定 Action。
  if (action.type === 'message/send') {
    setTimeout(() => {
      next(action);
    }, 1000);

    // 注意:
    // 这里没有立即调用 next。
    // 因此 Action 会暂时停留在 Middleware 中。
    return;
  }

  return next(action);
};

但是生产环境不要随意这样做。

因为它会改变:

text 复制代码
Action 的时序
状态更新时机
dispatch 返回值
错误传播
测试行为

所以应该根据明确的业务需求使用。


二十七、Middleware 可以修改 Action 吗?

技术上可以。

例如:

javascript 复制代码
const normalizeMiddleware = () => next => action => {
  const normalizedAction = {
    ...action,
    meta: {
      ...action.meta,
      timestamp: Date.now()
    }
  };

  return next(normalizedAction);
};

但是需要注意:

Middleware 能修改 Action,不代表应该随意修改 Action。

更推荐:

javascript 复制代码
const newAction = {
  ...action,
  meta: ...
};

next(newAction);

而不是:

javascript 复制代码
action.meta.timestamp = Date.now();
next(action);

避免直接修改调用方传入的数据。


二十八、Middleware 的执行顺序是什么?

面试题 7:多个 Redux Middleware 的执行顺序是什么?

假设:

javascript 复制代码
applyMiddleware(
  middlewareA,
  middlewareB,
  middlewareC
);

执行:

javascript 复制代码
store.dispatch(action);

前置逻辑:

text 复制代码
A
 ↓
B
 ↓
C
 ↓
Reducer

后置逻辑:

text 复制代码
Reducer
 ↓
C
 ↓
B
 ↓
A

也就是:

javascript 复制代码
const middlewareA = () => next => action => {
  console.log('A before');

  const result = next(action);

  console.log('A after');

  return result;
};

类似于:

text 复制代码
A before
  ↓
B before
  ↓
C before
  ↓
Reducer
  ↓
C after
  ↓
B after
  ↓
A after

这就是典型的洋葱模型。


二十九、为什么 Middleware 的顺序很重要?

因为 Middleware 之间可能存在依赖。

例如:

javascript 复制代码
applyMiddleware(
  thunkMiddleware,
  loggerMiddleware
);

和:

javascript 复制代码
applyMiddleware(
  loggerMiddleware,
  thunkMiddleware
);

执行效果可能不同。

尤其是 Logger:

text 复制代码
dispatch(function)

如果 Logger 只能处理普通对象:

javascript 复制代码
console.log(action.type);

那么它遇到函数可能出现问题。

所以 Middleware 的顺序不是随便排列的。


三十、Redux Middleware 与 React Redux 是什么关系?

这是明确区分的一个知识点。

正确理解:

Middleware 属于 Redux,而不是 React Redux。

关系:

text 复制代码
React
  ↓
React Redux
  ↓
Redux Store
  ↓
Redux Middleware
  ↓
Reducer

React Redux 主要负责:

text 复制代码
React ↔ Redux Store

而 Redux Middleware 负责:

text 复制代码
dispatch → Middleware → Reducer

所以面试时最好说:

"Redux Middleware 是 Redux Store 的扩展机制,React Redux 只是负责把 Redux Store 与 React 组件连接起来。"


三十一、applyMiddleware 是什么?

面试题 8:applyMiddleware 是什么?

核心思路(一句话)

applyMiddleware 是 Redux 提供的 Store Enhancer,用来把多个 Middleware 组合起来,并增强 Store 的 dispatch。

applyMiddleware 不是 Middleware 本身,而是一个 Store Enhancer。

这是面试中的关键概念。


三十二、Store Enhancer 和 Middleware 的关系

text 复制代码
Middleware
    ↓
描述"如何处理 Action"

而:

text 复制代码
applyMiddleware
    ↓
负责把 Middleware 安装到 Store

所以:

text 复制代码
Middleware
     ↓
applyMiddleware
     ↓
增强 Store
     ↓
增强 dispatch

三十三、完整 Redux 示例

下面给一个比较完整的面试级示例。

javascript 复制代码
import {
  createStore,
  applyMiddleware
} from 'redux';

/**
 * ==========================================
 * 1. 初始 State
 * ==========================================
 */
const initialState = {
  count: 0,
  loading: false,
  user: null,
  error: null
};

/**
 * ==========================================
 * 2. Reducer
 * ==========================================
 *
 * Reducer 只负责:
 *
 * oldState + action
 *        ↓
 *    newState
 *
 * 不处理网络请求、定时器等副作用。
 */
function reducer(state = initialState, action) {
  switch (action.type) {
    case 'counter/increment':
      return {
        ...state,

        // 根据 Action 修改 State。
        count: state.count + 1
      };

    case 'user/fetchStart':
      return {
        ...state,

        loading: true,
        error: null
      };

    case 'user/fetchSuccess':
      return {
        ...state,

        loading: false,
        user: action.payload
      };

    case 'user/fetchFailure':
      return {
        ...state,

        loading: false,
        error: action.error
      };

    default:
      return state;
  }
}

/**
 * ==========================================
 * 3. Logger Middleware
 * ==========================================
 */
const loggerMiddleware = ({ getState }) => next => action => {
  // 获取 Action 执行前的 State。
  const previousState = getState();

  console.log('--- Action Before ---');
  console.log('Action:', action);
  console.log('Previous State:', previousState);

  // 让 Action 继续向后传递。
  const result = next(action);

  // next 返回以后,
  // Reducer 和后面的 Middleware 已经执行完成。
  const nextState = getState();

  console.log('--- Action After ---');
  console.log('Next State:', nextState);

  // 把结果继续向上返回。
  return result;
};

/**
 * ==========================================
 * 4. 简化版 Thunk Middleware
 * ==========================================
 */
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
  // 如果 Action 是函数,
  // 就执行函数。
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }

  // 普通 Action 继续向后传递。
  return next(action);
};

/**
 * ==========================================
 * 5. 创建 Store
 * ==========================================
 *
 * applyMiddleware 会:
 *
 * loggerMiddleware
 *        +
 * thunkMiddleware
 *        ↓
 * 增强 store.dispatch
 */
const store = createStore(
  reducer,
  applyMiddleware(
    loggerMiddleware,
    thunkMiddleware
  )
);

/**
 * ==========================================
 * 6. 派发普通 Action
 * ==========================================
 */
store.dispatch({
  type: 'counter/increment'
});

/**
 * ==========================================
 * 7. 派发函数 Action
 * ==========================================
 *
 * 因为安装了 thunkMiddleware,
 * 所以现在 dispatch 可以接收函数。
 */
store.dispatch(async (dispatch, getState) => {
  // 读取当前 State。
  console.log('当前 count:', getState().count);

  // 告诉 Redux:
  // 用户数据开始加载。
  dispatch({
    type: 'user/fetchStart'
  });

  try {
    // 模拟异步请求。
    const user = await new Promise(resolve => {
      setTimeout(() => {
        resolve({
          id: 1,
          name: '张三'
        });
      }, 1000);
    });

    // 异步操作完成后,
    // 再派发一个普通 Action。
    dispatch({
      type: 'user/fetchSuccess',
      payload: user
    });
  } catch (error) {
    // 请求失败时,
    // 派发错误 Action。
    dispatch({
      type: 'user/fetchFailure',
      error: String(error)
    });
  }
});

三十四、完整执行流程

执行:

javascript 复制代码
store.dispatch(asyncFunction);

流程:

text 复制代码
                store.dispatch
                       ↓
              Logger Middleware
                       ↓
              Thunk Middleware
                       ↓
              判断 Action 类型
                       ↓
                  是函数
                       ↓
          执行 asyncFunction
                       ↓
              发起异步请求
                       ↓
                 请求完成
                       ↓
       dispatch({ type: 'user/fetchSuccess' })
                       ↓
              重新进入 Middleware 链
                       ↓
              Logger Middleware
                       ↓
              Thunk Middleware
                       ↓
                 原始 dispatch
                       ↓
                    Reducer
                       ↓
                  更新 State

这就是 Redux Middleware 的完整闭环。


三十五、Redux Middleware 的边界场景

1. Middleware 忘记调用 next

javascript 复制代码
const badMiddleware = () => next => action => {
  console.log(action);

  // 忘记 next(action)
};

那么:

text 复制代码
Action
  ↓
Middleware
  ↓
停止

Reducer 收不到 Action。

所以:

如果 Middleware 既不处理 Action,又希望 Action 继续传播,就必须调用 next(action)。


2. Middleware 重复调用 next

错误:

javascript 复制代码
const badMiddleware = () => next => action => {
  next(action);
  next(action);
};

可能导致:

text 复制代码
Reducer 执行两次

或者引发更复杂的状态问题。

所以一个 Middleware 对一个 Action 通常应该只调用一次 next。


三十六、Middleware 中递归调用 dispatch 的风险

例如:

javascript 复制代码
const badMiddleware = ({ dispatch }) => next => action => {
  if (action.type === 'A') {
    dispatch({
      type: 'A'
    });
  }

  return next(action);
};

会产生:

text 复制代码
A
 ↓
Middleware
 ↓
dispatch(A)
 ↓
Middleware
 ↓
dispatch(A)
 ↓
Middleware
 ↓
无限循环

因此使用:

javascript 复制代码
dispatch()

时必须考虑:

新的 Action 是否会再次触发当前 Middleware?

这是生产环境非常重要的边界问题。


三十七、next 和 dispatch 的面试对比表

对比项 next(action) dispatch(action)
作用 向下传递当前 Action 重新派发 Action
起点 当前 Middleware 整个 Middleware 链起点
是否经过当前 Middleware 不会再次经过当前 Middleware 会再次经过
是否经过前面的 Middleware 不会 会
是否可能触发 Reducer 是 是
常见用途 继续传递当前 Action 产生新的 Action
典型场景 Logger Thunk 异步完成后派发结果

一句话:

next 是"继续这次旅程",dispatch 是"重新开始一次旅程"。


三十八、Middleware 最常见的使用场景

场景 1:日志

text 复制代码
Action
 ↓
Logger
 ↓
Reducer

用于:

  • 调试
  • 状态变化追踪
  • 开发环境日志

场景 2:异步请求

text 复制代码
dispatch(thunk)
      ↓
Thunk
      ↓
API
      ↓
dispatch(success)
      ↓
Reducer

场景 3:错误监控

text 复制代码
Action
 ↓
Error Middleware
 ↓
捕获异常 / 上报
 ↓
next
 ↓
Reducer

场景 4:埋点

例如:

text 复制代码
user/login
product/view
order/create

统一记录:

text 复制代码
用户行为
Action
时间
页面
请求结果

场景 5:权限控制

text 复制代码
dispatch(action)
       ↓
权限 Middleware
       ↓
有权限?
   ↙       ↘
 否         是
 ↓          ↓
阻止       next

场景 6:Action 转换

例如给 Action 自动增加:

javascript 复制代码
{
  meta: {
    timestamp: Date.now()
  }
}

三十九、Redux Middleware 和 Redux Toolkit 的关系

现代 Redux 开发中还需要知道这一点。

现在官方更推荐使用:

Redux Toolkit

而不是手动:

javascript 复制代码
createStore()
applyMiddleware()

在 Redux Toolkit 中,通常通过:

javascript 复制代码
configureStore()

配置 Middleware。

例如:

javascript 复制代码
import { configureStore } from '@reduxjs/toolkit';

const store = configureStore({
  reducer: {
    // reducer 配置
  }
});

Redux Toolkit 默认会提供一组常用 Middleware,其中包括对 Redux 常见错误使用方式的开发期检查,并默认包含 Redux Thunk。

所以现代项目中通常:

text 复制代码
Redux Toolkit
      ↓
configureStore
      ↓
Middleware

而不是自己手动拼装所有基础设施。


四十、Redux Thunk 和 Redux Saga 怎么理解?

Redux Thunk

核心思想:

text 复制代码
dispatch(function)
       ↓
执行函数
       ↓
dispatch Action

适合:

  • 普通异步请求
  • 请求前后状态管理
  • 简单业务流程

Redux Saga

核心思想是使用 Generator 等机制,把复杂副作用流程抽离出来统一管理。

更适合:

text 复制代码
复杂异步流程
多个请求之间的协调
取消任务
并发控制
竞态处理
复杂副作用编排

但不要简单说:

"Thunk 不复杂,Saga 就一定更高级。"

两者解决问题的模型不同。


四十一、Redux Middleware 的主要矛盾和次要矛盾

这是你要求的结构化重点,我建议面试时直接这样总结。

主要矛盾

① Redux 核心流程必须保持可预测

text 复制代码
Action
 ↓
Reducer
 ↓
State

Reducer 应该保持纯粹。


② 真实应用又必须处理大量副作用

text 复制代码
API
日志
埋点
权限
WebSocket
错误处理
异步任务

③ Middleware 就是两者之间的桥梁

text 复制代码
副作用
 ↓
Middleware
 ↓
普通 Action
 ↓
Reducer

次要矛盾

① Middleware 执行顺序

text 复制代码
A → B → C

顺序会影响行为。

② next 与 dispatch 的区别

text 复制代码
next     → 当前链继续向后
dispatch → 从链头重新开始

③ Middleware 返回值

javascript 复制代码
return next(action);

否则可能改变:

javascript 复制代码
store.dispatch()

的返回结果。

④ Middleware 重复调用或者不调用 next

可能造成:

text 复制代码
Action 丢失
Action 重复
状态更新异常

⑤ Middleware 内部再次 dispatch

要避免:

text 复制代码
无限循环

四十二、面试官可能继续追问:Middleware 是 AOP 吗?

可以说:

Redux Middleware 在思想上与面向切面编程中的横切关注点比较接近,但不能简单等同于面向切面编程。

因为它可以统一处理:

text 复制代码
日志
监控
权限
异常
埋点

这些都是典型的横切逻辑。


四十三、面试官可能追问:Middleware 和 Express Middleware 有什么区别?

两者思想类似:

text 复制代码
Express:

Request
 ↓
Middleware
 ↓
Middleware
 ↓
Route Handler

Redux:

text 复制代码
Action
 ↓
Middleware
 ↓
Middleware
 ↓
Reducer

共同点:

text 复制代码
责任链
可组合
可以在流程中插入处理逻辑

但它们处理的对象不同:

text 复制代码
Express → HTTP Request / Response
Redux   → Action / Store Dispatch

所以可以用 Express / Koa 做类比,但不要说两者实现完全相同。


四十四、一个非常容易被问到的问题:Middleware 能访问 Reducer 吗?

严格来说:

Middleware 并不会直接调用某一个 Reducer。

它主要通过:

javascript 复制代码
next(action)

把 Action 向后传递。

最终由 Redux 的内部 dispatch 流程调用:

text 复制代码
Reducer

所以 Middleware 与 Reducer 是:

text 复制代码
Middleware
    ↓
dispatch pipeline
    ↓
Reducer

而不是:

text 复制代码
Middleware
    ↓
直接调用 reducer()

四十五、一个更准确的 Redux Middleware 心智模型

不要死记:

text 复制代码
dispatch → middleware → reducer

建议记成:

text 复制代码
                  ┌──────────────┐
                  │ Middleware A │
                  └──────┬───────┘
                         │ next
                         ↓
                  ┌──────────────┐
                  │ Middleware B │
                  └──────┬───────┘
                         │ next
                         ↓
                  ┌──────────────┐
                  │ Middleware C │
                  └──────┬───────┘
                         │
                         ↓
                  ┌──────────────┐
                  │ 原始 dispatch │
                  └──────┬───────┘
                         ↓
                     Reducer
                         ↓
                       State

同时每个 Middleware 都可以:

text 复制代码
Action 进入
    ↓
前置逻辑
    ↓
next(action)
    ↓
后置逻辑

因此整个 Middleware 链实际上是:

text 复制代码
          A before
              ↓
          B before
              ↓
          C before
              ↓
            Reducer
              ↓
           C after
              ↓
           B after
              ↓
           A after

四十六、面试高频问题总结

Q1:什么是 Redux Middleware?

答案:

Redux Middleware 是 Redux 提供的扩展机制,本质上是对 dispatch 的函数式包装,使 Action 在进入 Reducer 之前可以执行日志、异步请求、权限校验、错误处理、埋点等额外逻辑,并且多个 Middleware 可以组合成责任链。


Q2:为什么需要 Middleware?

答案:

主要是为了解决 Redux 核心状态更新流程与副作用之间的矛盾。Reducer 应保持纯函数,而真实应用又存在大量网络请求、日志、埋点、权限等副作用,因此 Middleware 把这些横切逻辑从 Reducer 中抽离出来。


Q3:Middleware 的标准签名是什么?

javascript 复制代码
const middleware = ({ dispatch, getState }) => next => action => {
  return next(action);
};

三个层次:

text 复制代码
store API
   ↓
next
   ↓
action

Q4:next 是什么?

当前 Middleware 的下游处理函数。

javascript 复制代码
next(action);

会把当前 Action 继续交给下一个 Middleware,最后进入原始 dispatch。


Q5:dispatch 和 next 有什么区别?

text 复制代码
next(action)
→ 从当前 Middleware 继续向后

dispatch(action)
→ 从整个 Middleware 链重新开始

Q6:applyMiddleware 是什么?

Store Enhancer。

它负责把多个 Middleware 组合起来,并增强 Store 的 dispatch。


Q7:Middleware 为什么能让 dispatch 支持函数?

因为类似 Redux Thunk 的 Middleware 会判断:

javascript 复制代码
typeof action === 'function'

如果是函数:

javascript 复制代码
action(dispatch, getState);

否则:

javascript 复制代码
next(action);

Q8:Middleware 可以阻止 Action 吗?

可以。

只要不调用:

javascript 复制代码
next(action);

Action 就不会继续向 Reducer 传播。


Q9:Middleware 可以在 Reducer 执行之后做事情吗?

可以。

例如:

javascript 复制代码
const middleware = () => next => action => {
  console.log('before');

  const result = next(action);

  console.log('after');

  return result;
};

next() 后面的代码会在下游处理完成后执行。


Q10:多个 Middleware 的执行顺序是什么?

假设:

javascript 复制代码
applyMiddleware(A, B, C);

执行过程:

text 复制代码
A before
 ↓
B before
 ↓
C before
 ↓
Reducer
 ↓
C after
 ↓
B after
 ↓
A after

四十七、最终满分答案

下面这一段可以直接作为你面试时的标准口述答案。

面试题:请介绍一下 Redux Middleware 的工作原理。

核心思路(一句话)

Redux Middleware 本质上是对 dispatch 的可组合包装,它让 Action 在进入 Reducer 之前可以执行异步请求、日志、权限、埋点、错误处理等副作用,同时保持 Reducer 的纯函数特性。

解决方案流程

text 复制代码
store.dispatch(action)
        ↓
Middleware A
        ↓ next(action)
Middleware B
        ↓ next(action)
Middleware C
        ↓ next(action)
Redux 原始 dispatch
        ↓
Reducer
        ↓
更新 State
        ↓
通知订阅者

每个 Middleware 还可以在 next(action) 前后执行逻辑:

text 复制代码
A before
   ↓
B before
   ↓
C before
   ↓
Reducer
   ↓
C after
   ↓
B after
   ↓
A after

底层实现原理

Redux Middleware 的标准签名是:

javascript 复制代码
const middleware = ({ dispatch, getState }) => next => action => {
  // Middleware logic

  return next(action);
};

第一层接收 Store API:

javascript 复制代码
{
  dispatch,
  getState
}

第二层的 next 表示当前 Middleware 的下游处理函数。

第三层的 action 是当前正在处理的 Action。

Redux 通过 applyMiddleware 把多个 Middleware 组合起来,本质上重新包装 Store 原来的 dispatch:

text 复制代码
middlewareA(
  middlewareB(
    middlewareC(
      originalDispatch
    )
  )
)

所以最终调用:

javascript 复制代码
store.dispatch(action);

实际上会依次经过整个 Middleware 链。

next 和 dispatch 的区别

这是 Redux Middleware 最重要的面试点之一。

javascript 复制代码
next(action);

表示:

从当前 Middleware 继续向后传递当前 Action。

而:

javascript 复制代码
dispatch(action);

表示:

重新从整个 Middleware 链的起点派发一个 Action。

例如:

text 复制代码
A → B → C → Reducer

如果当前在 B:

javascript 复制代码
next(action);

会:

text 复制代码
B → C → Reducer

而:

javascript 复制代码
dispatch(action);

会:

text 复制代码
A → B → C → Reducer

因此 Redux Thunk 在异步请求完成后通常使用:

javascript 复制代码
dispatch({
  type: 'user/fetchSuccess',
  payload: user
});

让新的 Action 重新经过整个 Middleware 链。

Redux Thunk 的核心原理

Redux 原生 dispatch 主要处理普通 Action。

Thunk Middleware 扩展了这一能力:

javascript 复制代码
const thunkMiddleware = ({ dispatch, getState }) => next => action => {
  // 如果 Action 是函数,就执行函数。
  if (typeof action === 'function') {
    return action(dispatch, getState);
  }

  // 如果是普通 Action,则继续向后传递。
  return next(action);
};

因此可以:

javascript 复制代码
store.dispatch(async (dispatch, getState) => {
  dispatch({
    type: 'user/fetchStart'
  });

  const response = await fetch('/api/user');

  const user = await response.json();

  dispatch({
    type: 'user/fetchSuccess',
    payload: user
  });
});

整个流程是:

text 复制代码
dispatch(function)
      ↓
Thunk Middleware
      ↓
执行函数
      ↓
异步请求
      ↓
请求完成
      ↓
dispatch(fetchSuccess)
      ↓
重新进入 Middleware 链
      ↓
Reducer
      ↓
更新 State

为什么需要 Middleware?

主要矛盾是:

text 复制代码
Redux 希望 Reducer:
纯函数、可预测、只负责状态计算

真实业务又需要:
API、日志、埋点、权限、WebSocket、异常处理等副作用

因此:

text 复制代码
副作用
   ↓
Middleware
   ↓
普通 Action
   ↓
Reducer
   ↓
State

Middleware 就成为 Redux 核心状态更新流程与外部副作用之间的隔离层。

面试时还可以补充三个关键点

第一,applyMiddleware 本身不是 Middleware,而是 Redux 提供的 Store Enhancer,负责组合 Middleware 并增强 dispatch。

第二,Middleware 的顺序很重要:

text 复制代码
A before
↓
B before
↓
C before
↓
Reducer
↓
C after
↓
B after
↓
A after

第三,如果 Middleware 不调用:

javascript 复制代码
next(action);

当前 Action 就不会继续向后传递;如果错误地重复调用 next(action),可能导致同一个 Action 被处理多次;如果 Middleware 内部无条件 dispatch 相同类型的 Action,还可能造成无限循环。

所以从底层来看,Redux Middleware 的核心并不是"异步",而是:

text 复制代码
函数包装
    +
责任链
    +
dispatch 增强
    +
副作用隔离
    +
Middleware 组合

这几个点理解清楚,基本就掌握了 Redux Middleware 的工作原理。


最后给你一张"面试脑图"

text 复制代码
                    Redux Middleware
                           │
          ┌────────────────┼────────────────┐
          ↓                ↓                ↓
       为什么?          是什么?          怎么做?
          │                │                │
          ↓                ↓                ↓
    隔离副作用       包装 dispatch      applyMiddleware
    保持 Reducer     可组合责任链            │
    保持纯函数           │                   ↓
                         ↓             Middleware Chain
                 ({dispatch,getState})      │
                         ↓                   ↓
                       next              A → B → C
                         ↓                   │
                       action                ↓
                                      Original Dispatch
                                             ↓
                                          Reducer
                                             ↓
                                           State

                    最重要的追问
                         │
            ┌────────────┼─────────────┐
            ↓            ↓             ↓
        next vs      Middleware     dispatch
        dispatch      签名          函数 Action
            │            │             │
            ↓            ↓             ↓
        当前链继续    store API      Redux Thunk
        向后传递        ↓             异步逻辑
                      next
                      ↓
                    action

真正应该背下来的只有 5 个核心点:

  1. Middleware 本质:包装并增强 dispatch。
  2. 标准签名:({ dispatch, getState }) => next => action => {}。
  3. next(action):当前链继续向后;dispatch(action):从整个链重新开始。
  4. applyMiddleware:Store Enhancer,负责组合 Middleware。
  5. Middleware 的核心价值不是"异步",而是把副作用和横切逻辑从纯粹的 Reducer 状态计算中隔离出来。
相关推荐
小白学大数据6 小时前
拼多多反爬对抗实战:Scrapy 中间件化采集架构解析
开发语言·scrapy·中间件·架构
Wang's Blog9 小时前
Java 中间件之 RabbitMQ 快速入门: SpringAMQP 的 DirectExchange 路由模式
java·中间件·java-rabbitmq
七夜zippoe9 小时前
Agent 中间件架构:钩子链、插件注册与横切治理
ai·中间件·架构·agent·钩子链
谢亮_vipxieliang10 小时前
Go Gin 中间件与参数验证:从入门到实战
中间件·golang·gin
EatFan10 小时前
React 项目踩坑实录:useEffect 闭包陷阱、Context 性能陷阱与 React 18 升级避坑排查手册
前端·javascript·react.js·react·react hooks·useeffect·闭包陷阱
code_slave(码畜)15 小时前
微服务架构落地:消息队列架构设计(下篇)——消息堆积、死信治理与集群监控告警实战
spring boot·spring cloud·微服务·云原生·中间件·架构
吴爃20 小时前
小微企业 SRE 稳定性建设(四):消息链路与消费积压
中间件·容量规划·消息队列·sre
Wang's Blog2 天前
Java 中间件之 RabbitMQ 快速入门: 异步通讯的优缺点
java·中间件·java-rabbitmq
Wang's Blog2 天前
Java 中间件之 RabbitMQ 快速入门: MQ 常见技术选型对比
java·中间件·java-rabbitmq
Wang's Blog2 天前
Java 中间件之 RabbitMQ 快速入门: RabbitMQ 介绍与安装部署
java·中间件·java-rabbitmq