前端 Redux applyMiddleware 中间件链原理

本文档深入讲解 Redux applyMiddleware 的设计目标、中间件链执行流程、源码核心思想、关键设计点和常见中间件实现方式。重点用流程图把 dispatch(action) 如何一层层穿过 middleware,最终到达 reducer 的过程讲清楚。


目录

  1. [为什么 Redux 需要中间件](#为什么 Redux 需要中间件 "#1-%E4%B8%BA%E4%BB%80%E4%B9%88-redux-%E9%9C%80%E8%A6%81%E4%B8%AD%E9%97%B4%E4%BB%B6")
  2. 先看最终效果:中间件链全景图
  3. [Redux 原始数据流](#Redux 原始数据流 "#3-redux-%E5%8E%9F%E5%A7%8B%E6%95%B0%E6%8D%AE%E6%B5%81")
  4. [applyMiddleware 到底做了什么](#applyMiddleware 到底做了什么 "#4-applymiddleware-%E5%88%B0%E5%BA%95%E5%81%9A%E4%BA%86%E4%BB%80%E4%B9%88")
  5. 中间件函数签名拆解
  6. [compose 组合机制详解](#compose 组合机制详解 "#6-compose-%E7%BB%84%E5%90%88%E6%9C%BA%E5%88%B6%E8%AF%A6%E8%A7%A3")
  7. [dispatch 被增强的完整流程](#dispatch 被增强的完整流程 "#7-dispatch-%E8%A2%AB%E5%A2%9E%E5%BC%BA%E7%9A%84%E5%AE%8C%E6%95%B4%E6%B5%81%E7%A8%8B")
  8. 常见中间件执行顺序
  9. [手写 applyMiddleware](#手写 applyMiddleware "#9-%E6%89%8B%E5%86%99-applymiddleware")
  10. 典型中间件实现
  11. 关键设计点与源码思想
  12. 常见踩坑与最佳实践
  13. 面试与实战总结

1. 为什么 Redux 需要中间件

1.1 Redux 原本只处理同步 action

Redux 的核心非常纯粹:

text 复制代码
UI 触发 action
  ↓
store.dispatch(action)
  ↓
reducer 根据 action 计算新 state
  ↓
store 通知订阅者
  ↓
React 重新渲染

原始 Redux 只要求 dispatch 接收普通对象:

js 复制代码
store.dispatch({
  type: 'user/login',
  payload: { username: 'Tom' },
})

也就是说,默认情况下 Redux 只认识这种 action:

text 复制代码
普通 JS 对象 plain object

1.2 真实业务不只是同步改状态

真实前端业务里,dispatch 之前或之后经常要做很多副作用:

  • 打日志;
  • 异步请求接口;
  • 埋点上报;
  • 错误捕获;
  • 权限校验;
  • action 格式校验;
  • loading 状态管理;
  • promise 展开;
  • thunk 函数执行;
  • crash report 上报。

如果这些逻辑都写在组件里,组件会越来越乱:

js 复制代码
async function handleLogin() {
  console.log('login start')
  dispatch({ type: 'login/start' })

  try {
    const user = await api.login()
    dispatch({ type: 'login/success', payload: user })
  } catch (err) {
    reportError(err)
    dispatch({ type: 'login/fail', error: err })
  }
}

中间件就是为了解决这个问题:

text 复制代码
把 dispatch 前后通用的副作用逻辑,从组件和 reducer 中抽离出来。

1.3 中间件解决的核心问题

text 复制代码
组件只负责发出意图
  ↓
中间件负责处理副作用、异步、日志、监控、拦截
  ↓
reducer 仍然保持纯函数,只负责计算 state

一句话:

text 复制代码
Redux middleware 是 dispatch 的增强器。

它不改 reducer,不改 state 结构,而是改造 dispatch 的行为。


2. 先看最终效果:中间件链全景图

2.1 加了中间件后的完整链路

假设我们注册了三个中间件:

js 复制代码
const store = createStore(
  reducer,
  applyMiddleware(logger, thunk, crashReporter)
)

最终 dispatch 流程大概是:

text 复制代码
组件调用 dispatch(action)
        │
        ▼
┌──────────────────────────────┐
│ logger middleware             │
│ - 记录 action 前 state         │
│ - 调用 next(action)            │
│ - 记录 action 后 state         │
└───────────────┬──────────────┘
                │ next(action)
                ▼
┌──────────────────────────────┐
│ thunk middleware              │
│ - 如果 action 是函数,就执行它  │
│ - 如果 action 是对象,就放行    │
└───────────────┬──────────────┘
                │ next(action)
                ▼
┌──────────────────────────────┐
│ crashReporter middleware      │
│ - try/catch 包住后续 dispatch  │
│ - 捕获异常并上报               │
└───────────────┬──────────────┘
                │ next(action)
                ▼
┌──────────────────────────────┐
│ 原始 store.dispatch            │
│ - 校验 action                  │
│ - 调用 reducer                 │
│ - 更新 state                   │
│ - 通知 listener                │
└───────────────┬──────────────┘
                │
                ▼
           React 重新渲染

2.2 洋葱模型图

Redux 中间件链很像洋葱模型:

text 复制代码
                dispatch(action)
                      │
                      ▼
              ┌────────────────┐
              │ middleware A   │
              │  before        │
              │    ┌───────────┴──────┐
              │    │ middleware B     │
              │    │  before          │
              │    │    ┌─────────────┴────┐
              │    │    │ middleware C     │
              │    │    │  before          │
              │    │    │    ┌─────────────┴───┐
              │    │    │    │ originalDispatch │
              │    │    │    │ reducer 更新 state│
              │    │    │    └─────────────┬───┘
              │    │    │  after           │
              │    │    └─────────────┬────┘
              │    │  after           │
              │    └───────────┬──────┘
              │  after         │
              └────────────────┘

执行顺序:

text 复制代码
进入顺序:A before → B before → C before → reducer
返回顺序:C after → B after → A after

2.3 大白话理解

中间件链像公司审批流:

text 复制代码
员工提交申请 action
  ↓
主管 logger 看一眼并记录
  ↓
HR thunk 判断是不是特殊申请
  ↓
法务 crashReporter 兜底异常
  ↓
老板 reducer 真正拍板修改 state
  ↓
结果再一层层返回给前面的人

每个中间件都有三个选择:

  • 放行:next(action)
  • 拦截:不调用 next(action)
  • 改造:修改 action 后再 next(newAction)
  • 延迟:异步完成后再 dispatch 新 action;
  • 扩展:在 next(action) 前后加逻辑。

3. Redux 原始数据流

3.1 不使用中间件时

text 复制代码
React Component
      │
      │ store.dispatch(action)
      ▼
┌─────────────────┐
│ originalDispatch│
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ reducer         │
│ state + action  │
│ => nextState    │
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ store.state     │
│ 被替换为 nextState│
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│ listeners       │
│ 通知订阅者       │
└────────┬────────┘
         │
         ▼
React-Redux 触发组件更新

3.2 原始 dispatch 的特点

Redux 原始 dispatch 做的事情很少:

text 复制代码
1. 检查 action 是不是普通对象
2. 检查 action.type 是否存在
3. 调用 reducer(currentState, action)
4. 保存新的 state
5. 通知订阅函数
6. 返回 action

它不负责:

  • 请求接口;
  • 等待 Promise;
  • 执行函数 action;
  • 记录日志;
  • 捕获业务异常;
  • 自动重试;
  • 埋点上报。

这些能力都通过 middleware 扩展。


4. applyMiddleware 到底做了什么

4.1 applyMiddleware 的定位

applyMiddleware 不是 middleware 本身,而是一个 store enhancer。

text 复制代码
middleware:增强 dispatch 的具体插件
applyMiddleware:把多个 middleware 组合起来的工具
store enhancer:增强 createStore 能力的高阶函数

调用方式:

js 复制代码
const store = createStore(reducer, applyMiddleware(thunk, logger))

本质上等价于:

text 复制代码
createStore 被 applyMiddleware 包了一层
  ↓
先创建原始 store
  ↓
再用 middleware 链替换 store.dispatch
  ↓
返回增强后的 store

4.2 核心流程图

text 复制代码
applyMiddleware(logger, thunk, crashReporter)
        │
        ▼
返回 enhancer:函数(createStore) => 新 createStore
        │
        ▼
createStore(reducer, preloadedState)
        │
        ▼
先调用原始 createStore 创建 store
        │
        ▼
取出 store.getState 和原始 dispatch
        │
        ▼
构造 middlewareAPI = { getState, dispatch }
        │
        ▼
依次调用 middleware(middlewareAPI)
        │
        ▼
得到 chain = [loggerFn, thunkFn, crashReporterFn]
        │
        ▼
compose(...chain)(store.dispatch)
        │
        ▼
得到增强版 dispatch
        │
        ▼
返回 { ...store, dispatch: enhancedDispatch }

4.3 源码简化版

Redux applyMiddleware 核心可以简化成:

js 复制代码
function applyMiddleware(...middlewares) {
  return function enhancer(createStore) {
    return function newCreateStore(reducer, preloadedState) {
      const store = createStore(reducer, preloadedState)

      let dispatch = () => {
        throw new Error('Dispatching while constructing middleware is not allowed')
      }

      const middlewareAPI = {
        getState: store.getState,
        dispatch: (action, ...args) => dispatch(action, ...args),
      }

      const chain = middlewares.map(middleware => middleware(middlewareAPI))
      dispatch = compose(...chain)(store.dispatch)

      return {
        ...store,
        dispatch,
      }
    }
  }
}

这里最关键的两行:

js 复制代码
const chain = middlewares.map(middleware => middleware(middlewareAPI))
dispatch = compose(...chain)(store.dispatch)

含义:

text 复制代码
第一行:给每个 middleware 注入 getState 和 dispatch
第二行:把多个 middleware 包成一条 dispatch 调用链

5. 中间件函数签名拆解

5.1 标准 middleware 长什么样

Redux middleware 是三层函数:

js 复制代码
const middleware = storeAPI => next => action => {
  // before
  const result = next(action)
  // after
  return result
}

完整类型可以理解为:

text 复制代码
middleware({ getState, dispatch })(next)(action)

三层分别是:

text 复制代码
第一层:拿到 storeAPI
第二层:拿到 next,也就是下一个 middleware 的 dispatch
第三层:拿到 action,真正处理本次 dispatch

5.2 三层函数分别解决什么问题

text 复制代码
middleware = storeAPI => next => action => result
             │          │       │
             │          │       └─ 本次派发的 action
             │          └───────── 下一个中间件,或者原始 dispatch
             └──────────────────── getState 和 dispatch

第一层:storeAPI

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

用于:

  • 读取当前 state;
  • 派发新的 action;
  • 在异步任务里继续 dispatch;
  • 根据 state 判断是否拦截。

第二层:next

js 复制代码
const middleware = storeAPI => next => action => {}

next 表示链路中的下一棒:

text 复制代码
当前 middleware 调用 next(action),action 才能继续往后走。

如果不调用 next(action),action 就被拦截。

第三层:action

js 复制代码
const middleware = storeAPI => next => action => {}

本次 dispatch 的内容,可以是:

  • 普通对象;
  • 函数;
  • Promise;
  • 特殊协议对象;
  • 被中间件转换后的新 action。

最终原始 dispatch 只接受普通对象。

5.3 为什么要设计成三层函数

因为 Redux 要分阶段完成三件事:

text 复制代码
创建 store 时:注入 getState、dispatch
组合链路时:注入 next
真正 dispatch 时:传入 action

如果写成一个函数,很难同时做到:

  • 初始化时拿 store;
  • 组合时拿下一个 middleware;
  • 执行时拿 action;
  • 支持优雅的函数组合。

三层函数本质是函数式编程里的柯里化设计。


6. compose 组合机制详解

6.1 compose 是什么

compose 的作用是把多个函数从右到左组合起来。

js 复制代码
compose(f, g, h)(x)

等价于:

js 复制代码
f(g(h(x)))

Redux 中:

js 复制代码
compose(logger, thunk, crashReporter)(store.dispatch)

等价于:

js 复制代码
logger(thunk(crashReporter(store.dispatch)))

最终得到的是:

text 复制代码
被 logger 包住、里面是 thunk、再里面是 crashReporter、最里面是原始 dispatch 的增强 dispatch。

6.2 compose 流程图

text 复制代码
原始函数:

logger
thunk
crashReporter
originalDispatch

组合后:

logger(
  thunk(
    crashReporter(
      originalDispatch
    )
  )
)

视觉化:

text 复制代码
┌────────────────────────────────────────┐
│ logger                                 │
│  next = thunk(...)                     │
│  ┌──────────────────────────────────┐  │
│  │ thunk                            │  │
│  │  next = crashReporter(...)       │  │
│  │  ┌────────────────────────────┐  │  │
│  │  │ crashReporter              │  │  │
│  │  │  next = originalDispatch   │  │  │
│  │  └────────────────────────────┘  │  │
│  └──────────────────────────────────┘  │
└────────────────────────────────────────┘

6.3 compose 简化实现

js 复制代码
function compose(...funcs) {
  if (funcs.length === 0) {
    return arg => arg
  }

  if (funcs.length === 1) {
    return funcs[0]
  }

  return funcs.reduce(
    (a, b) => (...args) => a(b(...args))
  )
}

核心:

text 复制代码
把 [A, B, C] 组合成 A(B(C(dispatch)))

6.4 为什么注册顺序和执行顺序看起来不同

注册:

js 复制代码
applyMiddleware(A, B, C)

组合:

text 复制代码
A(B(C(originalDispatch)))

执行:

text 复制代码
dispatch(action)
  ↓
A 收到 action
  ↓
A 调 next(action)
  ↓
B 收到 action
  ↓
B 调 next(action)
  ↓
C 收到 action
  ↓
C 调 next(action)
  ↓
originalDispatch 收到 action

所以:

text 复制代码
中间件进入顺序和注册顺序一致。
中间件返回顺序和注册顺序相反。

7. dispatch 被增强的完整流程

7.1 初始化阶段

text 复制代码
createStore(reducer, applyMiddleware(A, B, C))
        │
        ▼
applyMiddleware 创建 enhancer
        │
        ▼
enhancer 包装原始 createStore
        │
        ▼
原始 createStore 创建 store
        │
        ▼
准备 middlewareAPI
        │
        ▼
A(middlewareAPI), B(middlewareAPI), C(middlewareAPI)
        │
        ▼
得到 chain: [A1, B1, C1]
        │
        ▼
compose(A1, B1, C1)(store.dispatch)
        │
        ▼
得到 enhancedDispatch
        │
        ▼
返回增强后的 store

7.2 运行阶段

当组件调用:

js 复制代码
store.dispatch({ type: 'todo/add', payload: '学习 Redux' })

实际执行的是增强后的 dispatch:

text 复制代码
store.dispatch(action)
      │
      ▼
┌────────────────────┐
│ A middleware       │
│ before             │
│ next(action)       │
│ after              │
└─────────┬──────────┘
          │
          ▼
┌────────────────────┐
│ B middleware       │
│ before             │
│ next(action)       │
│ after              │
└─────────┬──────────┘
          │
          ▼
┌────────────────────┐
│ C middleware       │
│ before             │
│ next(action)       │
│ after              │
└─────────┬──────────┘
          │
          ▼
┌────────────────────┐
│ originalDispatch   │
│ reducer 更新 state  │
└────────────────────┘

7.3 带 before/after 的详细执行顺序

假设三个中间件都长这样:

js 复制代码
const A = store => next => action => {
  console.log('A before')
  const result = next(action)
  console.log('A after')
  return result
}

注册:

js 复制代码
applyMiddleware(A, B, C)

输出顺序:

text 复制代码
A before
B before
C before
reducer run
C after
B after
A after

流程图:

text 复制代码
调用方向:
A before ──→ B before ──→ C before ──→ reducer
                                                │
返回方向:
A after  ←── B after  ←── C after  ←────────────┘

8. 常见中间件执行顺序

8.1 logger 中间件

js 复制代码
const logger = store => next => action => {
  console.log('prev state', store.getState())
  console.log('action', action)

  const result = next(action)

  console.log('next state', store.getState())
  return result
}

它的关键是:

text 复制代码
next(action) 前:拿到旧 state
next(action) 后:拿到新 state

8.2 thunk 中间件

js 复制代码
const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}

它让 dispatch 支持函数:

js 复制代码
dispatch(async (dispatch, getState) => {
  dispatch({ type: 'login/start' })
  const user = await api.login()
  dispatch({ type: 'login/success', payload: user })
})

8.3 crashReporter 中间件

js 复制代码
const crashReporter = store => next => action => {
  try {
    return next(action)
  } catch (err) {
    reportError(err, {
      action,
      state: store.getState(),
    })
    throw err
  }
}

它的作用:

text 复制代码
包住后续中间件和 reducer,一旦出错就捕获并上报。

8.4 顺序为什么重要

不同顺序会产生不同效果。

logger 在 thunk 前面

js 复制代码
applyMiddleware(logger, thunk)

如果 dispatch 一个函数:

js 复制代码
dispatch(fetchUser())

logger 会先看到函数 action。

thunk 在 logger 前面

js 复制代码
applyMiddleware(thunk, logger)

thunk 会先执行函数 action,函数内部派发的普通对象 action 才会被 logger 记录。

实际开发常见顺序:

js 复制代码
applyMiddleware(thunk, logger)

这样 logger 更容易记录真正进入 reducer 的普通 action。


9. 手写 applyMiddleware

9.1 最小版本

js 复制代码
function applyMiddleware(...middlewares) {
  return createStore => (reducer, preloadedState) => {
    const store = createStore(reducer, preloadedState)

    let dispatch = store.dispatch

    const middlewareAPI = {
      getState: store.getState,
      dispatch: action => dispatch(action),
    }

    const chain = middlewares.map(middleware => middleware(middlewareAPI))
    dispatch = compose(...chain)(store.dispatch)

    return {
      ...store,
      dispatch,
    }
  }
}

9.2 为什么 middlewareAPI.dispatch 要包一层

注意这里:

js 复制代码
dispatch: action => dispatch(action)

而不是:

js 复制代码
dispatch: store.dispatch

原因是:

text 复制代码
middleware 里拿到的 dispatch,应该永远指向最终增强后的 dispatch。

如果直接传 store.dispatch,中间件内部再次 dispatch 时,会绕过其他中间件。

9.3 初始化期间禁止 dispatch

Redux 源码里会先把 dispatch 设置成一个报错函数:

js 复制代码
let dispatch = () => {
  throw new Error('Dispatching while constructing your middleware is not allowed')
}

原因:

text 复制代码
中间件链还没组装完成,此时 dispatch 行为不完整。

如果中间件在初始化阶段就 dispatch,可能导致:

  • 部分中间件没生效;
  • 链路顺序混乱;
  • 状态更新不可预测;
  • 调试困难。

10. 典型中间件实现

10.1 logger:记录 action 和 state

js 复制代码
const logger = store => next => action => {
  const prevState = store.getState()
  console.group(action.type)
  console.log('prev state', prevState)
  console.log('action', action)

  const result = next(action)

  const nextState = store.getState()
  console.log('next state', nextState)
  console.groupEnd()

  return result
}

适合开发环境,生产环境要控制日志量。

10.2 thunk:支持异步函数 action

js 复制代码
const thunk = ({ dispatch, getState }) => next => action => {
  if (typeof action === 'function') {
    return action(dispatch, getState)
  }

  return next(action)
}

业务使用:

js 复制代码
const fetchUser = userId => async (dispatch, getState) => {
  dispatch({ type: 'user/fetchStart', payload: userId })

  try {
    const user = await api.getUser(userId)
    dispatch({ type: 'user/fetchSuccess', payload: user })
  } catch (err) {
    dispatch({ type: 'user/fetchFail', error: err })
  }
}

10.3 promiseMiddleware:支持 Promise action

js 复制代码
const promiseMiddleware = store => next => action => {
  if (action && typeof action.then === 'function') {
    return action.then(store.dispatch)
  }

  return next(action)
}

可以支持:

js 复制代码
dispatch(fetch('/api/user').then(res => ({
  type: 'user/success',
  payload: res.json(),
})))

但生产项目里更推荐 thunk、saga 或 RTK Query,因为 Promise action 的可维护性较弱。

10.4 loadingMiddleware:统一处理 loading

约定 action 带 meta:

js 复制代码
const action = {
  type: 'user/fetch',
  payload: api.getUser(),
  meta: {
    loadingKey: 'userDetail',
  },
}

中间件:

js 复制代码
const loadingMiddleware = ({ dispatch }) => next => async action => {
  const loadingKey = action.meta && action.meta.loadingKey
  if (!loadingKey) {
    return next(action)
  }

  dispatch({ type: 'loading/start', payload: loadingKey })

  try {
    const result = await next(action)
    return result
  } finally {
    dispatch({ type: 'loading/end', payload: loadingKey })
  }
}

关键点:

  • 必须保证 finally 执行;
  • loadingKey 要稳定;
  • 不要让 loading action 再触发自己造成循环。

10.5 analyticsMiddleware:埋点上报

js 复制代码
const analyticsMiddleware = store => next => action => {
  const result = next(action)

  if (action.meta && action.meta.analytics) {
    sendAnalytics({
      event: action.meta.analytics,
      actionType: action.type,
      state: store.getState(),
    })
  }

  return result
}

适合:

  • 页面行为上报;
  • 按钮点击上报;
  • 关键业务节点上报;
  • 状态变更后上报。

10.6 permissionMiddleware:权限拦截

js 复制代码
const permissionMiddleware = store => next => action => {
  const permission = action.meta && action.meta.permission
  if (!permission) {
    return next(action)
  }

  const state = store.getState()
  if (!state.auth.permissions.includes(permission)) {
    return next({
      type: 'permission/denied',
      payload: permission,
    })
  }

  return next(action)
}

这类中间件适合拦截业务行为,但不要滥用。组件层和路由层的权限控制仍然要保留。


11. 关键设计点与源码思想

11.1 中间件增强的是 dispatch,不是 reducer

Redux 的核心原则是 reducer 必须纯净:

text 复制代码
相同 state + action => 相同 nextState

副作用不能放 reducer:

  • 不能请求接口;
  • 不能改外部变量;
  • 不能写 localStorage;
  • 不能发埋点;
  • 不能生成不可预测随机结果。

中间件把副作用放在 dispatch 链路上,保证 reducer 仍然纯。

11.2 next 和 dispatch 不是同一个东西

这是理解中间件最关键的点。

text 复制代码
next(action):把 action 交给链路中的下一个中间件
store.dispatch(action):从整条增强链路的最外层重新开始派发

流程图:

text 复制代码
当前在 middleware B 内部

store.dispatch(newAction)
        │
        ▼
从 A 重新开始:A → B → C → reducer

next(action)
        │
        ▼
继续往后走:C → reducer

所以:

  • 想放行当前 action:用 next(action)
  • 想额外派发一个新 action:用 dispatch(newAction)
  • 如果用错,可能导致中间件重复执行或死循环。

11.3 中间件可以拦截 action

js 复制代码
const filterMiddleware = store => next => action => {
  if (action.type === 'debug/ignore') {
    return
  }

  return next(action)
}

不调用 next(action),reducer 就永远收不到这个 action。

适合:

  • 权限拦截;
  • 非法 action 拦截;
  • 过期请求拦截;
  • 重复请求拦截。

但要慎用,因为拦截会改变数据流,调试时要有日志。

11.4 中间件可以改写 action

js 复制代码
const normalizeMiddleware = store => next => action => {
  if (action.type === 'user/update') {
    return next({
      ...action,
      payload: normalizeUser(action.payload),
    })
  }

  return next(action)
}

适合做:

  • payload 规范化;
  • action meta 补充;
  • 兼容老 action;
  • 自动加 traceId。

但不要在中间件里做过重业务转换,否则 action 变得不可预测。

11.5 中间件可以延迟 action

js 复制代码
const delayMiddleware = store => next => action => {
  const delay = action.meta && action.meta.delay
  if (!delay) {
    return next(action)
  }

  setTimeout(() => {
    next(action)
  }, delay)
}

注意:延迟后如果组件已经卸载,仍然可能更新状态,所以复杂场景要配合请求取消或状态校验。

11.6 中间件顺序是 API 设计的一部分

中间件顺序不是随便排的。

一般建议:

text 复制代码
协议转换类:thunk / promise / saga
  ↓
业务拦截类:permission / auth / dedupe
  ↓
监控日志类:logger / analytics
  ↓
异常兜底类:crashReporter 可根据目标放最外层或最内层

具体顺序取决于你想观察的是:

  • 原始 action;
  • 转换后的 action;
  • 进入 reducer 的 action;
  • reducer 后的 state。

11.7 middlewareAPI.dispatch 使用闭包保证最终 dispatch

源码里这个设计非常关键:

js 复制代码
const middlewareAPI = {
  getState: store.getState,
  dispatch: (action, ...args) => dispatch(action, ...args),
}

dispatch 变量后面会被重新赋值为增强版 dispatch。

因为 JS 闭包捕获的是变量引用,所以中间件内部调用 dispatch 时,拿到的是最终增强后的 dispatch。

流程:

text 复制代码
初始化 middlewareAPI.dispatch
  ↓
它引用外层 dispatch 变量
  ↓
compose 完成后 dispatch 被改成 enhancedDispatch
  ↓
中间件内部调用 dispatch
  ↓
实际走 enhancedDispatch 完整链路

11.8 为什么 reducer 执行期间不能 dispatch

Redux 会禁止 reducer 执行期间再次 dispatch。

原因:

text 复制代码
reducer 正在计算新 state
  ↓
如果中途又 dispatch
  ↓
当前 state 到底是哪一个会变得混乱

reducer 必须保持纯函数,不应该在 reducer 中触发新的 action。


12. 常见踩坑与最佳实践

12.1 忘记 return next(action)

错误:

js 复制代码
const middleware = store => next => action => {
  next(action)
}

推荐:

js 复制代码
const middleware = store => next => action => {
  return next(action)
}

原因:

text 复制代码
dispatch 的返回值可能被调用方依赖。

例如 thunk 会返回异步函数的结果,组件可能会:

js 复制代码
await dispatch(fetchUser())

12.2 dispatch 和 next 用错导致死循环

错误:

js 复制代码
const middleware = store => next => action => {
  return store.dispatch(action)
}

这会让 action 从最外层重新进入当前中间件,形成死循环。

正确放行:

js 复制代码
return next(action)

只有额外派发新 action 时才用 store.dispatch(newAction)

12.3 中间件里吞掉 action

js 复制代码
const middleware = store => next => action => {
  if (action.type === 'x') {
    return
  }
  return next(action)
}

如果是有意拦截,必须写清楚日志或注释。否则 reducer 收不到 action,会很难排查。

12.4 异步中间件没有处理错误

js 复制代码
const middleware = store => next => async action => {
  const result = await api.call()
  return next({ type: 'success', payload: result })
}

如果接口失败,可能导致未捕获 Promise rejection。

推荐:

  • try/catch;
  • fail action;
  • 统一错误上报;
  • loading finally 关闭;
  • 调用方可感知返回值。

12.5 logger 顺序导致看不到真实 action

如果 logger 放在 thunk 前面,它会记录函数 action。

js 复制代码
applyMiddleware(logger, thunk)

如果想记录 reducer 真正处理的普通 action,更推荐:

js 复制代码
applyMiddleware(thunk, logger)

12.6 不要把所有业务都塞进 middleware

middleware 适合横切能力:

  • 日志;
  • 异步协议;
  • 监控;
  • 错误上报;
  • 权限拦截;
  • action 规范化。

不适合承载大量具体业务逻辑。具体业务逻辑更适合:

  • action creator;
  • thunk;
  • saga;
  • RTK Query;
  • service 层。

12.7 生产环境日志要谨慎

logger 在开发环境很好用,但生产环境可能带来:

  • 性能损耗;
  • 隐私泄漏;
  • 日志量过大;
  • 控制台污染;
  • 敏感信息暴露。

建议:

text 复制代码
开发环境启用详细 logger
生产环境只上报必要指标和异常
敏感字段要脱敏

13. 面试与实战总结

13.1 一句话解释 middleware

text 复制代码
Redux middleware 是对 dispatch 的增强,它位于 action 发出和 reducer 执行之间,用来处理异步、日志、错误上报、权限拦截等副作用逻辑。

13.2 一句话解释 applyMiddleware

text 复制代码
applyMiddleware 是 Redux 提供的 store enhancer,它会先创建原始 store,然后把多个 middleware 通过 compose 组合成一条链,最终替换 store.dispatch。

13.3 一句话解释中间件链

text 复制代码
中间件链本质是 A(B(C(originalDispatch))) 这样的函数嵌套,action 进入时按注册顺序经过中间件,返回时按相反顺序回到调用方。

13.4 必须掌握的关键点

  1. middleware 的签名是 storeAPI => next => action => result
  2. applyMiddleware 增强的是 dispatch
  3. next(action) 表示交给下一个中间件;
  4. dispatch(action) 表示从完整中间件链重新开始;
  5. compose(...chain)(store.dispatch) 生成最终增强 dispatch;
  6. 注册顺序决定进入顺序,返回顺序相反;
  7. thunk 让 dispatch 支持函数 action;
  8. logger 的位置会影响它看到的 action;
  9. 中间件可以拦截、改写、延迟、扩展 action;
  10. reducer 必须保持纯函数,副作用放在 middleware 或异步 action 中。

13.5 最终记忆版

text 复制代码
Redux 的原始 dispatch 只能处理普通对象 action。
applyMiddleware 的作用就是把 dispatch 包装成一条中间件链。
每个 middleware 都是 storeAPI => next => action 三层函数。
storeAPI 让中间件能读 state 和再次 dispatch。
next 代表链路中的下一棒。
action 是本次真正派发的内容。

多个 middleware 通过 compose 组合成:
A(B(C(originalDispatch)))。
所以 action 进入时是 A → B → C → reducer,返回时是 C → B → A。

最关键的区别是:
next(action) 是继续当前链路,dispatch(action) 是从完整链路重新开始。
理解这一点,就理解了 Redux 中间件链的核心。
相关推荐
英俊潇洒美少年1 小时前
Vue 生产环境打包:SourceMap、压缩、混淆、Gzip、多环境配置 企业级最佳实践
前端·javascript·vue.js
2601_957786771 小时前
企业矩阵运营的“三段论“:管号、产内容、获线索——全链路系统的价值拆解
java·前端·矩阵·多平台管理
城市的稻草人VS2 小时前
rust【日志库】
前端·rust
问心无愧05132 小时前
ctf show web 入门258
android·前端·笔记
qq_2518364572 小时前
基于java Web 耗材购置与维修网络申报审批系统设计与实现
java·开发语言·前端
UXbot2 小时前
企业AI开发工具:界面自动生成与前端代码交付能力详解
前端·人工智能·交互·web app·ui设计
专业技术员!!!!2 小时前
陪玩系统前端核心功能详解|线上线下陪玩平台开发方案
前端·陪玩系统·电竞陪玩
英俊潇洒美少年2 小时前
前端主流状态管理全家桶:Vuex/Pinia/Redux/Zustand/MobX 从入门到原理、实战、面试全解
前端·面试·职场和发展
Maddie_Mo2 小时前
Pi Agent Web 使用教程:把本地 Pi Coding Agent 搬进浏览器
android·java·前端·人工智能·ai