Redux中间件是如何拦截并增强dispatch的?源码级解析

来源:站长平台作者:菲律宾程序员头衔:程序员
导读:本期聚焦于菲律宾程序员创作的《Redux中间件是如何拦截并增强dispatch的?源码级解析》,敬请观看详情。Redux原生dispatch只接受携带type字段的普通对象,一旦传入函数或Promise就会在reducer内部抛出类型错误。中间件机制正是为突破这一限制而设计,它在store创建阶段通过applyMiddleware接管dispatch的生成过程。applyMiddleware利用compose把多个中间件函数从右到左组合起来,每个中间件拿到store的getState与dispatch接口,并接收下游next函数,返回新的dispatch实现。业务代码派发action时,会先经过最外层中间件的预处理、异步判断或日志输出,再逐层向下传递,最终到达原始dispatch完成状态更新。返回值则沿着同样的链路向上回溯,形成洋葱模型。本文从简化后的createStore与applyMiddleware源码切入,逐段解析中间件存储、函数组合和自定义实现方式,同时说明为什么中间件顺序会影响执行结果,以及在现代Redux项目中如何正确理解这一机制。

Redux中间件是连接同步reducer与各类副作用需求的桥梁。默认情况下,createStore返回的dispatch方法只会检查action是否为普通对象并且带有type属性,然后同步调用reducer计算新状态。如果开发者试图传入一个函数来延迟执行某个任务,例如在数据请求完成后再派发真实action,这套默认实现会直接报出Actions must be plain objects错误。中间件机制没有改动reducer和state结构,而是通过包装dispatch,让本来只能处理对象的方法获得执行异步逻辑、记录日志、捕获异常等扩展能力。下面进入源码细节。

Redux中间件是如何拦截并增强dispatch的?源码级解析

一、默认dispatch的边界与限制

要理解中间件为什么以这种形式出现,先要看清原生dispatch做了什么。Redux的createStore在内部维护当前状态、当前reducer和监听器列表,dispatch负责派发动作、更新状态并通知订阅者。一个简化后的核心实现大致如下。

function createStore(reducer, preloadedState, enhancer) {
  if (typeof preloadedState === 'function' && typeof enhancer === 'undefined') {
    enhancer = preloadedState;
    preloadedState = undefined;
  }
  if (typeof enhancer !== 'undefined') {
    if (typeof enhancer !== 'function') {
      throw new Error('Expected the enhancer to be a function.');
    }
    return enhancer(createStore)(reducer, preloadedState);
  }

  let currentReducer = reducer;
  let currentState = preloadedState;
  let currentListeners = [];
  let nextListeners = currentListeners;
  let isDispatching = false;

  function getState() {
    if (isDispatching) {
      throw new Error('You may not call store.getState() while the reducer is executing.');
    }
    return currentState;
  }

  function dispatch(action) {
    if (typeof action !== 'object' || action === null) {
      throw new Error('Actions must be plain objects. Use custom middleware for async actions.');
    }
    if (typeof action.type === 'undefined') {
      throw new Error('Actions may not have an undefined "type" property.');
    }
    if (isDispatching) {
      throw new Error('Reducers may not dispatch actions.');
    }
    try {
      isDispatching = true;
      currentState = currentReducer(currentState, action);
    } finally {
      isDispatching = false;
    }
    const listeners = currentListeners = nextListeners;
    for (let i = 0; i < listeners.length; i++) {
      const listener = listeners[i];
      listener();
    }
    return action;
  }

  return {
    dispatch,
    getState,
    subscribe,
    replaceReducer
  };
}

这段代码中最值得关注的是dispatch内部的校验逻辑。它先判断action必须是对象且不能为null,接着判断action.type不能是undefined。这意味着函数、Promise、字符串等非对象值都会被直接拒绝。更关键的是,dispatch会同步执行reducer,并在执行期间锁定isDispatching标志,防止reducer内部再次派发动作造成死循环。这种设计保证了状态变更的可预测性,但也把异步请求、定时器等副作用完全挡在Redux流程之外。

如果想在请求完成后再派发一个真正的action,常规做法是先调用store.dispatch发送一个pending动作,然后在回调中再手动调用store.dispatch发送success或failure动作。这种写法能够工作,但会让组件层和数据层耦合大量重复逻辑。中间件解决的正是这个问题,它不改变reducer,而是允许开发者定义一个接收store工具能力的函数,在action抵达原始dispatch之前或之后执行额外操作。

二、applyMiddleware与compose的组合机制

Redux官方提供的applyMiddleware函数是一个store增强器。createStore检测到enhancer参数存在时,就不再自行创建store,而是把创建权交给enhancer。applyMiddleware拿到createStore后,先创建一个临时的dispatch占位函数,然后为每个中间件注入getState和dispatch接口,再通过compose把中间件链组合成一个新的dispatch函数。

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)));
}

function applyMiddleware(...middlewares) {
  return createStore => (...args) => {
    const store = createStore(...args);
    let dispatch = () => {
      throw new Error('Dispatching while constructing your middleware is not allowed.');
    };

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

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

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

先看compose。它接收一组函数,使用reduce从右到左执行组合。当传入多个中间件时,compose会把最后一个中间件作为最内层,把第一个中间件作为最外层。组合后的结果依然是一个函数,这个函数接收原始store.dispatch,并返回经过所有中间件包装后的新dispatch。这种从右到左的组合方式形成了洋葱结构,请求从前到后经过每一层中间件,响应则从后到前依次返回。

applyMiddleware内部有几个容易忽略的细节。第一,middlewareAPI中的dispatch并不是原始store.dispatch,而是一个闭包引用。中间件在初始化阶段拿到的dispatch并不是最终包装后的dispatch,而是会在后续指向新生成的dispatch变量。这样设计是为了避免中间件在构建阶段调用dispatch导致状态错乱。第二,每个中间件都被调用一次并传入middlewareAPI,返回值是一个接收next函数的高阶函数。compose负责把这些next连接起来,每个next指向链条中更内层的dispatch实现。第三,最终返回的对象使用展开运算符保留store原有的getState、subscribe等方法,但用新dispatch覆盖了原始dispatch,因此调用方对store.dispatch的引用会指向增强后的版本。

中间件的三层柯里化结构也可以从这个源码中直接推导出来。第一层接收middlewareAPI,用于注入getState和dispatch;第二层接收next,用于表示下游处理函数;第三层接收action,用于处理具体的业务动作。如果中间件不调用next,action就不会继续向下传递,因此中间件具备拦截能力。如果中间件在调用next前后添加日志,就形成了日志记录。如果中间件判断action是一个函数并自行执行,就形成了异步thunk。

三、自定义中间件与执行顺序验证

理解源码之后,编写一个自定义中间件就变得非常直观。下面示例同时定义两个中间件,一个记录派发前后的日志,另一个处理函数类型的action。

const loggerMiddleware = store => next => action => {
  console.log('dispatching', action);
  const result = next(action);
  console.log('next state', store.getState());
  return result;
};

const thunkMiddleware = store => next => action => {
  if (typeof action === 'function') {
    return action(store.dispatch, store.getState);
  }
  return next(action);
};

loggerMiddleware在调用next之前打印当前action,在next返回后通过store.getState读取最新状态。thunkMiddleware检查action是否为函数,如果是函数就执行该函数,并把增强后的store.dispatch和store.getState传给它,否则把action交给下游。这两个中间件分别代表了同步增强和异步处理两种典型场景。

执行顺序可以通过组合这两个中间件来观察。假设又增加两个只输出日志的中间件,分别命名为firstMiddleware和secondMiddleware,并在createStore时按顺序传入。

const firstMiddleware = store => next => action => {
  console.log('first before');
  const result = next(action);
  console.log('first after');
  return result;
};

const secondMiddleware = store => next => action => {
  console.log('second before');
  const result = next(action);
  console.log('second after');
  return result;
};

const store = createStore(
  reducer,
  applyMiddleware(firstMiddleware, secondMiddleware)
);

store.dispatch({ type: 'INCREMENT' });
// 输出:
// first before
// second before
// second after
// first after

从输出可以清楚看到,第一个传入的中间件位于最外层,第二个位于内层。action先进入firstMiddleware,再进入secondMiddleware,最后到达原始dispatch更新状态。return值则按照完全相反的顺序回到最外层。如果某个中间件在调用next之前返回,下游逻辑不会执行;如果在调用next之后返回,上游中间件可以继续处理结果。这种结构非常适合实现日志、权限校验、错误边界、请求去重等复杂能力。

四、现代Redux中的中间件配置与注意事项

在实际项目中,现在更常见的是使用Redux Toolkit提供的configureStore。它内部已经默认包含了一些常用中间件,例如redux-thunk和开发环境下的状态不可变检查、序列化检查等。用户只需要通过middleware选项添加额外中间件,不再需要手动调用applyMiddleware。

import { configureStore } from '@reduxjs/toolkit';
import logger from 'redux-logger';

const store = configureStore({
  reducer: rootReducer,
  middleware: getDefaultMiddleware => getDefaultMiddleware().concat(logger)
});

但是读applyMiddleware源码仍然有实际价值。当排查中间件不生效、action数量异常、异步状态丢失等问题时,理解函数组合方向可以帮助快速定位。一个常见错误是把日志中间件放在异步中间件外层,但异步中间件又在处理函数action时手动调用了store.dispatch。此时日志中间件只能看到最初的函数action,看不到函数内部派发的真实action。原因很简单,函数内部调用的是增强后的store.dispatch,但这次调用会重新从最外层进入整个中间件链。如果日志中间件没有记录这个新的真实action,开发者就可能在控制台里只看到函数对象,误以为异步派发被吞掉。

另一个需要注意的地方是中间件的初始化时序。applyMiddleware在创建store时只会执行中间件的第一层和第二层,也就是middleware => next => action中的前两层。真正处理每个action的第三层函数是在组合完成后才被调用。因此不建议在中间件初始化阶段依赖最终dispatch行为,也不要在初始化阶段尝试读取业务状态并立即派发动作,否则很容易触发Dispatching while constructing错误。中间件应该保持纯函数式的结构,把可变逻辑放到最内层的action处理器中。

从源码层面看,Redux中间件并不是一个神秘的功能,它只是高阶函数和函数组合的巧妙应用。通过包装dispatch、注入store接口、连接next链路,它让原本只能同步处理普通对象的dispatch具备了近乎无限的扩展空间。掌握这套机制后,无论是使用redux-thunk处理异步,还是编写自定义日志、崩溃上报、权限拦截中间件,都能更清楚地判断每一步的数据流向和执行边界。

Redux中间件JavaScript状态管理源码解析修改时间:2026-09-29 00:24:08

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0929/63194.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。