React-Redux 的 batch() API:合并 React 渲染更新、优化多 action 批量分发实战指南
【免费下载链接】react-reduxOfficial React bindings for Redux项目地址: https://gitcode.com/gh_mirrors/re/react-redux
batch()是 React-Redux 自 v7.0.0 起公开导出的一个工具函数,其作用是把同一事件循环内、位于 React 事件系统之外的多次状态更新合并为一次渲染提交。本文以仓库中 v7.0 版 batch 文档 为主体骨架,结合当前仓库的源码实现(src/utils/batch.ts、src/exports.ts、src/utils/Subscription.ts)与测试用例,讲解batch()的由来、用法、内部实现与演进,帮助读者理解在 React 18 之前如何避免"一次分发多次渲染"的性能浪费,以及 React 18 之后该 API 为何退化为无操作(no-op)。
一、batch()是什么
batch()是 React-Redux 在 v7.0.0 中新增并公开导出的 API,函数签名如下:
batch(fn: Function)它接收一个回调函数,并在调用期间把其中产生的所有 React 状态更新收集起来,合并为单次渲染提交。官方文档给出的定位非常明确:当你在 React 事件处理器之外分发多个 Redux action 时,用它来保证"多个 action 只引起一次渲染更新"。
import { batch } from 'react-redux' function myThunk() { return (dispatch, getState) => { // should only result in one combined re-render, not two batch(() => { dispatch(increment()) dispatch(increment()) }) } }在这个 thunk 示例中,batch()回调内的两次dispatch(increment())虽然依次触发了两次 store 更新通知,但最终只会导致订阅组件被合并成一次重渲染,而不是两次。
二、为什么要设计batch():React 批处理机制的背景
要理解batch()的价值,需要先了解 React 的批处理(batching)机制。React 的unstable_batchedUpdates()API 允许把同一事件循环 tick 内的所有 React 更新合并到单次渲染提交中。关键事实是:
- React内部已经在自己的事件处理回调中对更新做了批处理;
- 但该 API 属于renderer 包(如 ReactDOM、React Native),而非 React 核心本身。
也就是说,在 React 18 的自动批处理(automatic batching)出现之前,批处理只发生在 React 自己管理的事件路径上。对于 Redux 这类"外部状态源"来说,常见的非 React 路径包括:
setTimeout/setInterval回调;- Promise 的
.then()与async/await后续代码; - 原生 DOM 事件监听器;
- WebSocket / SSE 消息回调;
- Redux-thunk、Redux-Saga 等中间件中异步触发的连续分发。
在这些路径里连续dispatch多个 action,React 18 之前的 ReactDOM 会为每次 store 更新触发一次同步渲染,导致多余的重渲染开销。这正是 React-Redux 暴露batch()的直接原因。
三、React-Redux 如何把batch()提供给用户
由于 React-Redux 需要同时运行在 ReactDOM 和 React Native 两种环境下,而批处理 API 又位于各自的 renderer 包中,React-Redux 在构建时负责从正确的 renderer 导入该 API 供内部使用,并把它重命名为batch()对外公开。
这一点可以从仓库的 API 报告文件 etc/react-redux.api.md 中得到佐证,其顶部对batch的来源声明为:
import { unstable_batchedUpdates as batch } from 'react-dom'也就是说,在 v7 时代,batch本质上就是 ReactDOM 的unstable_batchedUpdates的再导出——它在构建期被固定绑定到正确的 renderer 实现,从而屏蔽了 ReactDOM 与 React Native 之间的差异。
同时,batch与Provider、connect、useSelector等一起被列在包的主入口导出清单中,见 src/exports.ts:
const batch = defaultNoopBatch export { Provider, batch, connect, legacy_connect, shallowEqual }四、源码视角:batch()在 React-Redux 内部的真实用法
batch()不只是给用户用的工具,它也是 React-Redux 订阅通知机制的核心环节。在 src/utils/Subscription.ts 中,订阅模块把defaultNoopBatch以batch的别名导入,并用它包裹整条监听器通知链:
import { defaultNoopBatch as batch } from './batch' // ... notify() { batch(() => { let listener = first while (listener) { listener.callback() listener = listener.next } }) },这段代码的含义是:当 store 状态变化时,Subscription会遍历所有已订阅的监听器(对应各个connect组件或 hooks 消费者),而整轮遍历被放在batch()回调内执行。这样,同一轮 store 更新触发的所有组件通知会在一个批处理单元内完成,从而保证祖先组件先于后代组件重渲染(订阅模块注释中明确写明了这一自上而下的更新保证),并让多个被通知组件共享一次渲染提交。
测试代码也印证了这一内部行为。在 test/components/Provider.spec.tsx 中,测试注释直接写道 "Provider uses unstable_batchedUpdates() under the hood"(Provider 底层使用了unstable_batchedUpdates()),并在rtl.act()中连续分发 action 后断言子组件只被调用一次。此外,test/components/connect.spec.tsx 中也有 "setState calls DOM handlers are batched"(DOM 事件处理器中的 setState 调用会被批处理)的断言。
五、演进:从unstable_batchedUpdates到 no-op
值得特别说明的是,当前仓库(v9.x)中的batch已经是一个立即执行回调的 no-op 实现,其定义见 src/utils/batch.ts:
// Default to a dummy "batch" implementation that just runs the callback export function defaultNoopBatch(callback: () => void) { callback() }src/exports.ts中也对batch标注了明确的弃用说明(见 src/exports.ts):
/** * @deprecated As of React 18, batching is enabled by default for ReactDOM and React Native. * This is now a no-op that immediately runs the callback. */ const batch = defaultNoopBatch这一演进对应 React 18 的自动批处理(automatic batching)特性:React 18 起,无论更新在何处排队,ReactDOM 与 React Native 都会自动批量处理所有状态更新。因此,当前版本官方文档 docs/api/batch.md 明确提示:
如果你正在使用 React 18,你不需要使用
batchAPI。React 18 会自动批处理所有状态更新,无论它们在何处排队。
换句话说,batch()的历史使命是弥补 React 18 之前"React 事件系统之外不自动批处理"的缺口;React 18 之后该缺口被框架本身填平,batch()保留下来仅仅是为了向后兼容——调用它只会立即执行回调,不会产生额外效果。需要说明的是,上述"自动批处理"能力来源于 React 18 的官方设计,而 React-Redux 侧以 no-op 承接兼容性,这一事实以 src/utils/batch.ts 与 src/exports.ts 的代码为准。
六、实用指南:什么场景还需要(或不需要)batch()
综合上述演进,可以给出如下使用结论:
React 18(含 createRoot)之前的环境:
- 在 React 事件处理器、生命周期之外连续分发多个 action(如 thunk 内的连续 dispatch、定时器回调、异步请求完成后的多次 dispatch),建议用
batch(() => { ... })包裹,把多次渲染合并为一次; - 示例即本文第一节的 thunk 代码,注释明确写出"应该只产生一次合并重渲染,而不是两次"。
React 18 及之后的环境:
- 不需要手动调用
batch(),框架已自动批处理; - 调用
batch()也不会出错,它只是立即执行回调(no-op),可作为过渡期代码保持兼容。
边界与限制:
- 当前仓库为 React Server Components 提供了独立入口 src/index-rsc.ts,其中
batch与Provider、connect、hooks 等一样被替换为throwNotSupportedError——在 RSC 环境中调用batch会直接抛出错误并提示"此导出仅支持在 Client Component 中使用"; batch()只合并同一回调内排队的更新;跨回调(如两个独立的setTimeout)的更新不会被合并;- 包的依赖与运行环境前提见 package.json 的
peerDependencies(react: ^18.0 || ^19、redux: ^5.0.0),并结合 tsup.config.ts 中多目标构建产物理解其在不同模块格式下的分发方式。
七、延伸阅读指引
原文档 v7.0 版 batch 文档 末尾附有两条参考来源,此处以文字形式说明其内容要点(不在文章中嵌入外部链接):
- React 官方关于
unstable_batchedUpdates()API 的引入提交,是理解该 API 设计意图的第一手资料; - React 18 工作组关于"Automatic Batching for Fewer Renders"(自动批处理、更少渲染)的公开讨论(编号 #21),详细阐述了 React 18 如何把批处理扩展到所有更新场景,也是
batch()退化为 no-op 的官方背景。
仓库内同一主题的更多版本资料还包括 v7.1 版 batch 文档、v7.2 版 batch 文档 以及当前的 docs/api/batch.md,内容一致,可对照阅读以了解该 API 在版本演进中的措辞变化(如 v7.1 文档中的unstable_batchedUpdate()与unstable_batchedUpdates()拼写差异,即属历史版本遗留)。
总结
batch()是 React-Redux 为应对 React 18 之前"React 事件系统之外不自动批处理"这一限制而提供的官方工具:它把unstable_batchedUpdates从正确的 renderer 在构建期导入并公开重导出,内部用它包裹订阅通知循环以保证批量渲染,外部用它帮助用户在 thunk、异步回调等场景合并多次 dispatch 引发的渲染。到了 React 18 的自动批处理时代,它按设计退化为立即执行回调的 no-op,以最小代价保持 API 兼容。理解这条从"必要工具"到"兼容占位"的演进线,既能写出更高效的 React-Redux 代码,也能准确判断新老版本项目各自的最优实践。
【免费下载链接】react-reduxOfficial React bindings for Redux项目地址: https://gitcode.com/gh_mirrors/re/react-redux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考