理解React中的状态批处理与更新队列

前言

之前作者写过一篇文章,初步介绍了在React中,是如何进行渲染,这篇文章讲讲渲染前要做的批处理和更新队列

"在 React 中,状态更新不是命令式的立即执行,而是声明式的加入队列。" - React 核心原则

从一道题开始

观察下列代码,你认为点击button时h1中的number会变成几?

js 复制代码
import { useState } from 'react';

export default function Counter() {
  const [number, setNumber] = useState(0);

  return (
    <>
      <h1>{number}</h1>
      <button onClick={() => {
        setNumber(number + 1);
        setNumber(number + 1);
        setNumber(number + 1);
      }}>+3</button>
    </>
  )
}

我们发现,即使在onClick事件处理函数中setNumber(number+1)三次,但是我们每次点击"+3"按钮后,数字会只增加1。

那么这到底是为什么呢,让我接下来为你揭秘其背后的原理!

React 的批处理机制

什么是批处理?

React 不会在每次 setState 调用后立即更新组件并重新渲染。相反,它会将同一事件处理函数中的所有状态更新收集起来放入队列 ,批量处理后再一次性更新 UI(也就是渲染) 。这种优化策略称为批处理(batching)

比如上面的例子

  • 第一次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列

  • 第二次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列

  • 第三次setNumber(number + 1); 此时不会渲染,会将该状态更新加入队列

  • onClick整个事件处理函数 完成之后,此时它才会批量处理队列中的刚刚添加的所有状态更新

  • 处理完状态更新之后,它此时会重渲染,从而我们能看到页面中的0变成了1

而至于为什么是变成1而不是3,其实是闭包的原因!

闭包如何捕获状态值

首先我们要明确我们React在初始会进行初次渲染,此时是基于state是为0的环境下渲染的

所以在整个onClick事件处理函数开始时,number初始为0,我们在页面上看到的也是0

js 复制代码
const [number, setNumber] = useState(0);

在JavaScript中,闭包会记住创建它时的外部变量值。在事件处理函数onClick被创建时,它捕获了当前的number值(0)。在事件触发时,虽然状态可能已经发生了变化,但在这个闭包中,number仍然是创建时的值(0)。因此,多次调用setNumber(number + 1)都使用0作为基础值。

所以上面的过程实际上是:

  1. 所有三个 setNumber 调用使用的 number 都是同一个闭包捕获的值 (初始值 0),所以刚刚提到的将状态加入队列:在这里是将setNumber(0 + 1);加入队列
  2. 执行完事件处理函数onClick后,在队列中处理状态更新,连续执行了三次 setNumber(1)
  3. 此时我们重渲染,就将基于state=1的结果渲染,导致我们在页面上看到内容为1

图形演示:

如何解决闭包的问题呢

我们接下来就要解决闭包的带来的问题,使得在点击按钮"+3"时,能够如愿地让0变成3。

解决方式是使用函数式更新: 将onClick改为如下:

js 复制代码
// 由原来的 setNumber(number + 1)改为了setNumber(n => n + 1);
<button onClick={() => {
  setNumber(n => n + 1);
  setNumber(n => n + 1);
  setNumber(n => n + 1);
}}>+3</button>

我们能看到,将值传递,改为传递更新函数(n => n+1) 就能解决问题。

这是为什么呢?

这里的批处理过程仍然和上面的是一样,

  • 每次setNumber(n => n + 1)时不会重渲染页面,而是将这个状态更新加入到队列中。
  • 当整个事件处理函数onClick完成之后,再按顺序处理现在队列
  • 队列里是刚刚加入的三个setNumber(n => n + 1),按顺序执行

只不过这里不会受闭包的影响

(n => n+1)该函数每次更新都基于前一次更新后的状态,而不是一直记住初始的number。

  • 第一次:prevState => prevState + 1 -> 0+1=1

  • 第二次:prevState => prevState + 1 -> 1+1=2

  • 第三次:prevState => prevState + 1 -> 2+1=3

函数式更新不依赖于闭包中捕获的状态变量,而是依赖于React内部维护的更新队列。React会保证在应用更新时,每个更新函数都能接收到最新的、已经过之前更新修改的状态。

它的过程是这样的:

需要注意

在函数式更新时,在每次将状态更新加入队列的过程中,n 的值此时并未改变只有在事件处理函数完全执行完毕后,React 才会批量处理队列中的更新函数此时 n 的值才会改变。

区别

最后总结一下,函数式更新和直接更新的区别:

特性 直接更新 (setNumber(number+1)) 函数式更新 (setNumber(n=>n+1))
使用的状态值 当前渲染周期的固定值 (闭包捕获) React 内部待处理的最新状态
更新依据 基于渲染时的状态快照 基于前一个更新计算的结果
多次更新结果 会被合并覆盖 (+1) 形成更新链 (+3)
闭包依赖 是 (使用外部变量) 否 (使用参数 n)

批处理的混合场景分析

好了,通过上面的分析,你应该能够知道批处理的整个流程以及其中的规则。

下面给出几个复杂场景的应用,根据上面的规则,你应该能够分析出来这几个场景的结果:

4.1 场景一:先替换后更新

js 复制代码
<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
}}>

执行过程:

  1. setNumber(number + 5) → 加入队列:"替换为 5"
  2. setNumber(n => n + 1) → 加入队列:更新函数

根据刚刚提到的,函数式更新会记住前面的状态,所以,更新队列处理:

更新队列 n 返回值
"替换为 5" - 5
n => n + 1 5 6

最终结果:6

4.2 场景二:混合更新后替换

js 复制代码
<button onClick={() => {
  setNumber(number + 5);
  setNumber(n => n + 1);
  setNumber(42);
}}>

执行过程:

  1. setNumber(number + 5) → "替换为 5"
  2. setNumber(n => n + 1) → 更新函数
  3. setNumber(42) → "替换为 42"

这里要注意:按队列的顺序执行,所以最后一步是直接将值替换为42,会完全覆盖当前的状态

更新队列处理:

更新队列 n 返回值
"替换为 5" - 5
n => n + 1 5 6
"替换为 42" - 42

最终结果:42(最后一步替换了之前的所有更新)

批处理的好处

  • 性能优化:减少不必要的渲染次数
  • 避免"半成品"渲染:确保 UI 更新基于完整的状态变更
  • 维护状态一致性:防止部分状态更新导致界面不一致

总结:掌握状态更新队列

理解 React 的状态批处理和更新队列机制是成为高级 React 开发者的关键一步:

  1. 批处理机制:React 会收集事件处理函数中的所有状态更新,批量处理后再更新 UI
  2. 闭包陷阱:直接使用状态值更新会捕获过时的状态快照
  3. 函数式更新:通过传递更新函数 (n => n + 1) 解决闭包问题
  4. 更新队列:React 按顺序执行更新队列中的函数,每个函数接收前一个更新的结果
  5. 混合更新:直接更新 ("替换为 X") 会重置更新队列

通过合理使用函数式更新,我们可以精确控制状态变更流程,避免常见的状态更新陷阱,构建更加健壮的 React 应用。

🌇结尾

本文部分内容:参考React官方文档

感谢你看到最后,最后再说两点~

①如果你持有不同的看法,欢迎你在文章下方进行留言、评论。

②如果对你有帮助,或者你认可的话,欢迎给个小点赞,支持一下~
我是3Katrina,一个热爱编程的大三学生

(文章内容仅供学习参考,如有侵权,非常抱歉,请立即联系作者删除。)

作者:3Katrina

来源:稀土掘金

著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

相关推荐
Jackson_Mseven4 小时前
面试官:useEffect 为什么总背刺?我:闭包、ref 和依赖数组的三角恋
前端·react.js·面试
前端小盆友6 小时前
从零实现一个GPT 【React + Express】--- 【2】实现对话流和停止生成
前端·gpt·react.js
OLong7 小时前
2025年最强React插件,支持大量快捷操作
前端·react.js·visual studio code
摸鱼仙人~7 小时前
重塑智能体决策路径:深入理解 ReAct 框架
前端·react.js·前端框架
namehu8 小时前
浏览器中的扫码枪:从需求到踩坑再到优雅解决
前端·react.js
杨进军8 小时前
React 使用 MessageChannel 实现异步更新
react.js
namehu8 小时前
浏览器中的打印魔法:Lodop与系统打印机
前端·react.js
如果'\'真能转义说8 小时前
React自学 基础一
前端·react.js·前端框架
巴巴_羊8 小时前
react setstate
react.js
患得患失9498 小时前
【前端】【React】【数字孪生】全解react使用threejs,@react-three/fiber,@react-three/drei
前端·react.js·前端框架