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 是不是函数;如果是函数,就执行它并注入 dispatchgetState;如果不是,就继续调用 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?

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


三十七、nextdispatch 的面试对比表

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

顺序会影响行为。

nextdispatch 的区别

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:dispatchnext 有什么区别?

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 链。

nextdispatch 的区别

这是 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 状态计算中隔离出来。
相关推荐
今年下半年7 小时前
SpringBoot整合金蝶中间件
spring boot·中间件·maven
青山木11 小时前
RocketMQ 入门到原理(六):可靠性全景
java·后端·中间件·架构·rocketmq
青山木11 小时前
RocketMQ 入门到原理(五):特殊消息类型
java·后端·中间件·架构·rocketmq
FungLeo12 小时前
成为全栈·React 管理后台篇·Markdown 编辑器:预览、暗色主题与连续图片粘贴
图片上传·react·响应式设计·markdown编辑器·异步编程·成为全栈
青山木1 天前
RocketMQ 入门到原理(三):消息存储原理
java·后端·中间件·架构·rocketmq
FungLeo1 天前
成为全栈·React 管理后台篇·表单页范式:校验、数据回填与未保存保护
react·表单设计·zod·react hook form·成为全栈·数据回填
名字还没想好☜1 天前
React 文件上传实战:预览 URL 回收、多文件逐个进度与拖拽放置区踩坑
前端·react·next.js
FungLeo1 天前
成为全栈·React 管理后台篇·列表页范式:让分页、筛选和返回位置进入 URL
react·前端架构·分页查询·react router·成为全栈·url状态
FungLeo2 天前
成为全栈·React 管理后台篇·服务端状态、会话状态、界面状态:不要都塞进 Zustand
react·状态管理·zustand·react hook form·成为全栈·tanstack query