一、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 个核心点:
- Middleware 本质:包装并增强
dispatch。 - 标准签名:
({ dispatch, getState }) => next => action => {}。 next(action):当前链继续向后;dispatch(action):从整个链重新开始。applyMiddleware:Store Enhancer,负责组合 Middleware。- Middleware 的核心价值不是"异步",而是把副作用和横切逻辑从纯粹的 Reducer 状态计算中隔离出来。