当前位置: 首页 > article >正文

从 Vue 到 React:深入理解 useState 的异步更新

目录

  • 从 Vue 到 React:深入理解 useState 的异步更新与函数式写法
    • 1. Vue 的响应式回顾:每次赋值立即生效
    • 2. React 的状态更新是异步且批量的
      • 原因解析
    • 3. 函数式更新:唯一的正确写法
    • 4. 对比 Vue vs React 状态更新
    • 5. React `useState` 的核心源码机制
      • 1️⃣ Hook 数据结构:链式存储的 Hook 节点
      • 2️⃣ 初次渲染:挂载阶段的 useState
      • 3️⃣ 更新过程:将 `action` 推入队列
      • 4️⃣ 更新应用:render 阶段的 `updateState`
      • 5️⃣ 函数式更新为何正确?
      • 6️⃣ 总结一下
  • 延申
    • 1. 什么是 Fiber
      • Fiber 在 Hook 中的作用
    • 2. 中断式渲染(Interruptible Rendering)
      • 📌 举个例子
      • Vue 有中断渲染吗?
      • 区别总结

从 Vue 到 React:深入理解 useState 的异步更新与函数式写法

在从 Vue 转向 React 的过程中,很容易被一个看似简单的问题困扰:

setCount(count + 1);
setCount(count + 1); // 预期 +2,实际只 +1?

为什么我们连续两次调用 setCount(count + 1),却没有得到我们预期的 +2 效果?而换成函数式写法:

setCount(prev => prev + 1);
setCount(prev => prev + 1); // 结果才是 +2

却又一切正常?

本文将从 Vue 的响应式系统出发,一步步理解 React 中useState 的状态更新机制,并且在文末会附上核心源码解析,帮你更深入地理解。


1. Vue 的响应式回顾:每次赋值立即生效

在 Vue 中,响应式数据是实时变更的:

const count = ref(0);
count.value++;
count.value++; // 最终为 2

Vue 是利用 Proxy 拦截 .value 的修改,每次赋值都会立即生效并触发响应式更新,从开发者来看就是所写即所得

2. React 的状态更新是异步且批量的

React 的状态更新行为则截然不同。以 useState 为例:

const [count, setCount] = useState(0);

如果我们连续两次执行:

setCount(count + 1);
setCount(count + 1);

会发现页面上 count 只增加了 1

原因解析

React 为了性能优化,在一次事件循环中会合并所有的 setState 操作(批处理 / batching),并且这些更新是异步生效的。也就是说:

  • 多次 setCount(count + 1) 实际上使用的是同一个旧值 count
  • 每次 render 周期中,state 是只读快照,相当于每次 render 周期会给 count 拍一张照片,照片停格在 1 ,而非 2

结果就是:

const count = 0;
setCount(count + 1); // 相当于 setCount(1)
setCount(count + 1); // 还是 setCount(1)

最终只更新一次。

3. 函数式更新:唯一的正确写法

为了解决这个问题,React 提供了 函数式更新写法

setCount(prev => prev + 1);
setCount(prev => prev + 1); // 最终为 2

这种写法的优势在于:每次执行都会传入 最新的 state 值,即使处于同一个批处理中,也能逐步叠加。

它的工作方式等价于:

let current = count;
current = current + 1;
current = current + 1;
setCount(current);

4. 对比 Vue vs React 状态更新

特性VueReact
响应性实现Proxy 拦截或 ref()Fiber 链表 + Hook 存储
多次状态修改同步生效,立即响应异步合并更新(batch)
闭包问题很少遇到高频出现,需小心处理
正确累加方式count.value++setCount(prev => prev + 1)

5. React useState 的核心源码机制

让我们再深入一步,了解 useState 背后是如何工作的。

useState 的底层逻辑,本质上是通过构建一个单向链表结构的 Hook 存储系统,结合更新队列与调度策略来驱动状态更新。我们从以下几个维度拆解其机制:


1️⃣ Hook 数据结构:链式存储的 Hook 节点

在 React 函数组件中,每调用一次 useState(或其他 Hook),React 就在当前组件 Fiber 节点上注册一个对应的 Hook 节点,结构大致如下:( 关于 Fiber 这个概念,文末会有讲解 )

type Hook = {memoizedState: any; // 当前 state 值queue: UpdateQueue | null; // 更新队列(待应用的 state 变更)next: Hook | null; // 指向下一个 Hook(形成链表)
}

每个组件内部维护着一个单向链表的 Hook 列表,通过「调用顺序」来标识唯一性。

⚠️ 注意:不能写条件调用 Hook(如 if (...) useState()),否则链表顺序不一致,状态错位。


2️⃣ 初次渲染:挂载阶段的 useState

在组件初次渲染时,React 调用 mountState 来创建 Hook 节点:

function mountState(initialState) {const hook = mountWorkInProgressHook();hook.memoizedState = typeof initialState === 'function'? initialState(): initialState;hook.queue = {pending: null, // 更新链表为空dispatch: null,lastRenderedReducer: basicStateReducer};const dispatch = (hook.queue.dispatch = (action) => {// 将 action 推入队列const update = {action,next: null};enqueueUpdate(hook.queue, update);scheduleRender(); // 触发调度});return [hook.memoizedState, dispatch];
}
  • hook.memoizedState:保存当前状态值
  • hook.queue:保存更新 action 的链表队列
  • dispatch:即我们使用的 setState

3️⃣ 更新过程:将 action 推入队列

当你调用 setState 时,实际发生的是:

dispatch(action);

然后内部调用:

const update = {action, // 可以是函数或值next: null
};enqueueUpdate(queue, update); // 插入环状链表
scheduleRender(); // 触发一次组件更新调度

更新队列为 循环单向链表(circular linked list),便于在 render 阶段完整遍历。


4️⃣ 更新应用:render 阶段的 updateState

组件重新渲染时,React 调用 updateState,核心逻辑如下:

function updateState() {const hook = updateWorkInProgressHook();const queue = hook.queue;let newState = hook.memoizedState;let update = queue.pending;if (update !== null) {// 进入环形队列的遍历let first = update.next;let current = first;do {const action = current.action;newState = typeof action === 'function'? action(newState) // 函数式更新(prev => next): action;current = current.next;} while (current !== first);hook.memoizedState = newState;queue.pending = null; // 清空队列}return [hook.memoizedState, queue.dispatch];
}

💡 关键点:

  • 如果 action 是函数,就使用函数式更新
  • 更新是基于前一次的 state 累加
  • 最终更新 memoizedState,用于本轮 render

5️⃣ 函数式更新为何正确?

因为 action 是一个函数,且传入的是队列中最新的 newState,每次都基于上一个结果计算:

setCount(prev => prev + 1);
setCount(prev => prev + 1); // prev 已是前一次递增后的值

这就是为什么 函数式写法可以连续叠加更新,而直接写 count + 1 会导致旧值闭包。


6️⃣ 总结一下

[初始化]└─ mountState → 创建 Hook 节点并保存初始值[调用 setState]└─ dispatch(action) → enqueueUpdate() → queue 中插入更新节点[下一次 render]└─ updateState() 遍历队列 → 应用每个更新 → 更新 memoizedState[完成更新]└─ React 触发重渲 → 组件拿到新 state → UI 重新渲染

所以呢总的来说就是:

React 中每次调用 useState 实际是在组件内部构建一个 Hook 链表节点,该节点保存当前的状态值与更新队列。在调用 setState 时,更新被推入队列,在下一轮 render 时遍历这些更新并依次应用。函数式写法 setState(prev => ...) 能够正确地累加,是因为每次都基于最新的状态进行计算,这是解决闭包陷阱的核心。


延申

1. 什么是 Fiber

在上面的介绍中,我们提到了 Fiber。接下来就来讲讲它。

在 Vue 中,组件更新是“同步递归”的,数据变化会同步触发整棵组件树的遍历。而 React 为了支持中断式渲染(interruptible rendering)任务调度优化,从 React 16 起引入了一个新的内部架构:Fiber

等等,这里又提到了一个新概念 中断式渲染,这又是啥?为什么要中断?怎么中断?
先放一边,后面讲解。

Fiber 是 React 内部对组件状态和更新过程的抽象,它是一个轻量级的工作单元,构成了组件树的替代表示。可以简单理解为:

  • Vue 中每个组件有一个对应的 VNode
  • React 中每个组件有一个对应的 FiberNode
  • 每个 FiberNode 上挂载了组件的状态、更新队列、DOM 引用等所有运行时信息

当状态变更时,React 会基于当前 Fiber 创建一个「工作中的 Fiber(WorkInProgress)」来调度、计算和构建新的 UI,整个过程是可中断的,方便浏览器优先处理用户交互。

Fiber 在 Hook 中的作用

对于 useState 而言,每一个 Hook 都是绑定在当前组件的 FiberNode 上的:

  • 一个组件 = 一个 FiberNode
  • 一个组件中所有 Hook = FiberNode 内部的 Hook 链表

这就构成了 useState 工作机制的运行基础。

好问题!“中断式渲染” 是 React 和 Vue 在响应式更新机制上的一个本质区别。下面我来给你系统地解释下这个概念,并对比 Vue 与 React 的设计思路。


2. 中断式渲染(Interruptible Rendering)

什么是中断式渲染?中断式渲染指的是:React 在进行组件渲染/更新过程中,可以在某个阶段“暂停”当前任务,把控制权交还给浏览器,之后再“恢复”渲染,继续未完成的工作。

React为什么能有这种能力,就源于我们上面讲到的 Fiber 架构,渲染任务可以变成一个一个可中断的小单元(FiberNode),React 能够控制这些任务的优先级、切换、重试等等。

📌 举个例子

假设你的页面中有一个复杂的表单组件,更新非常耗时。如果在更新中用户点击了一个按钮,传统的同步渲染方式会“卡住”一段时间,直到更新完成才响应用户事件。

但在 React 中,如果这个表单更新是一个低优先级任务,那么它可以被打断,React 会:

  1. 暂停当前渲染
  2. 先去响应点击事件
  3. 然后再继续渲染未完成的表单组件

这种机制就能提升用户的体验,避免了长时间的卡顿。


Vue 有中断渲染吗?

在 Vue 中,渲染机制是同步的,一旦开始渲染就会一直进行到完成。

虽然 Vue 3 引入了一些优化和改进,比如 Composition API 和更好的 Proxy 响应式系统,但它仍然是同步渲染的。

// Vue 中更新触发后会:
updateComponent = () => {vm._update(vm._render()); // 同步递归执行 render + patch
}

所以:

  • 虽然 Vue 更新也是批处理(利用 nextTick 合并)
  • 一旦进入组件渲染,就会一直渲染到结束,中途不能被打断

区别总结

特性React (Fiber)Vue
渲染过程是否可中断✅ 可以(可中断、恢复)❌ 不可以(同步递归)
是否支持任务优先级✅ 支持优先级调度(如 startTransition❌ 不支持
使用场景大型组件树、长列表渲染、动画过渡等小型中型项目、同步响应
架构设计Fiber 链表、调度器、异步渲染栈式递归、响应式依赖跟踪

相关文章:

从 Vue 到 React:深入理解 useState 的异步更新

目录 从 Vue 到 React:深入理解 useState 的异步更新与函数式写法1. Vue 的响应式回顾:每次赋值立即生效2. React 的状态更新是异步且批量的原因解析 3. 函数式更新:唯一的正确写法4. 对比 Vue vs React 状态更新5. React useState 的核心源码…...

Java使用ANTLR4对Lua脚本语法校验

文章目录 什么是ANTLR?第一个例子ANTLR4 的工作流程Lua脚本语法校验准备一个Lua Grammar文件maven配置生成Lexer Parser Listener Visitor代码新建实体类Lua语法遍历器语法错误监听器单元测试 参考 什么是ANTLR? https://www.antlr.org/ ANTLR (ANothe…...

vue3.2 + element-plus 实现跟随input输入框的弹框,弹框里可以分组或tab形式显示选项

效果 基础用法&#xff08;分组选项&#xff09; 高级用法&#xff08;带Tab栏&#xff09; <!-- 弹窗跟随通用组件 SmartSelector.vue --> <!-- 弹窗跟随通用组件 --> <template><div class"smart-selector-container"><el-popove…...

Vue 2.0和3.0笔记

Vue 3 关于组件 今天回顾了下2.0关于组件的内容&#xff0c;3.0定义组件的方式多了一种就是通过单文件组件&#xff08;Single-File Component&#xff09;的方式将Vue的模板&#xff0c;逻辑和样式放到一个文件中&#xff0c;2.0则不同&#xff0c;它是将模板放到一个属性中…...

Windows VsCode Terminal窗口使用Linux命令

背景描述&#xff1a; 平时开发环境以Linux系统为主&#xff0c;有时又需要使用Windows系统下开发环境&#xff0c;为了能像Linux系统那样用Windows VsCode&#xff0c;Terminal命令行是必不可少内容。 注&#xff1a;Windows11 VsCode 1.99.2 下面介绍&#xff0c;如何在V…...

负载均衡的实现方式有哪些?

负载均衡实现方式常见的有: 软件负载均衡、硬件负载均衡、DNS负载均衡 扩展 二层负载均衡&#xff1a;在数据链路层&#xff0c;基于MAC地址进行流量分发&#xff0c;较少见于实际应用中 三层负载均衡&#xff1a;在网络层&#xff0c;基于IP地址来分配流量&#xff0c;例如某…...

Oracle 中的 NOAUDIT CREATE SESSION 命令详解

Oracle 中的 NOAUDIT CREATE SESSION 命令详解 NOAUDIT CREATE SESSION 是 Oracle 数据库中用于取消对用户登录会话审计的命令&#xff0c;它与 AUDIT CREATE SESSION 命令相对应。 一、基本语法 NOAUDIT CREATE SESSION [BY user1 [, user2]... | BY [SESSION | ACCESS]] …...

OutputStreamWriter 终极解析与记忆指南

OutputStreamWriter 终极解析与记忆指南 一、核心本质 OutputStreamWriter 是 Java 提供的字符到字节的桥梁流&#xff0c;属于 Writer 的子类&#xff0c;负责将字符流按指定编码转换为字节流。 注意&#xff1a;OutputStreamWriter也是一个字符流&#xff0c;也是一个转换…...

1022 Digital Library

1022 Digital Library 分数 30 全屏浏览 切换布局 作者 CHEN, Yue 单位 浙江大学 A Digital Library contains millions of books, stored according to their titles, authors, key words of their abstracts, publishers, and published years. Each book is assigned an u…...

LWIP学习笔记

TCP/ip协议结构分层 传输层简记 TCP&#xff1a;可靠性强&#xff0c;有重传机制 UDP&#xff1a;单传机制&#xff0c;不可靠 UDP在ip层分片 TCP在传输层分包 应用层传输层网络层&#xff0c;构成LWIP内核程序&#xff1a; 链路层&#xff1b;由mac内核STM芯片的片上外设…...

Nodejs Express框架

参考&#xff1a;Node.js Express 框架 | 菜鸟教程 第一个 Express 框架实例 接下来我们使用 Express 框架来输出 "Hello World"。 以下实例中我们引入了 express 模块&#xff0c;并在客户端发起请求后&#xff0c;响应 "Hello World" 字符串。 创建 e…...

LeetCode面试热题150中19-22题学习笔记(用Java语言描述)

Day 04 19、最后一个单词的长度 需求&#xff1a;给你一个字符串 s&#xff0c;由若干单词组成&#xff0c;单词前后用一些空格字符隔开。返回字符串中 最后一个 单词的长度。 单词 是指仅由字母组成、不包含任何空格字符的最大子字符串。 代码表示 public class Q19_1 {p…...

道路运输安全员企业负责人考试内容与范围

道路运输企业主要负责人&#xff08;安全员&#xff09;考证要求 的详细说明&#xff0c;适用于企业法定代表人、分管安全负责人等需取得的 《道路运输企业主要负责人和安全生产管理人员安全考核合格证明》&#xff08;交通运输部要求&#xff09;。 考试内容与范围 1. 法律法…...

Visual Studio Code 开发 树莓派 pico

开发环境 MCU&#xff1a;Pico1&#xff08;无wifi版&#xff09;使用固件&#xff1a;自编译版本开发环境&#xff1a;Windows 10开发工具&#xff1a;Visual Studio Code 1.99.2开发语言&#xff1a;MicroPython & C 插件安装 找到Raspberry Pi Pico并安装开启科学上网…...

Oracle 11G RAC 删除添加节点(一):删除节点

1、查看节点删除前的资源状态 用集群资源查看命令查看一下状态 1 [gridlvmrac1 ~]$crsctl stat res ‐t 2 ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐ ‐…...

面试宝典(C++基础)-02

文章目录 1.C++基础1.1 说说new和malloc的区别1.2 说说const和define的区别。1.3 说说C++中函数指针和指针函数的区别1.4 说说const int *a, int const *a, const int a, int *const a, const int *consta分别是什么,有什么特点。1.5 说说使用指针需要注意什么?1.6 说说内联函…...

express框架使用cors包解决跨域问题时,还是存在问题的原因。

express框架使用cors包解决跨域问题时&#xff0c;还是存在问题的原因。 今天我在使用express框架写一个后台管理系统时&#xff0c;发现存在这样的问题&#xff0c;那就是跨域问题&#xff0c;但是我明明是使用了 cors 包解决了跨域问题了。当我调用其他接口的时候&#xff0…...

Python与R语言用XGBOOST、NLTK、LASSO、决策树、聚类分析电商平台评论信息数据集

全文链接&#xff1a;https://tecdat.cn/?p41501 分析师&#xff1a;Rui Liu 在当今数字化浪潮席卷的时代&#xff0c;电商市场的蓬勃发展犹如一部波澜壮阔的史诗&#xff0c;蕴藏着无尽的商业价值与潜力。电商平台积累的海量数据&#xff0c;宛如一座等待挖掘的宝藏&#xff…...

半导体制造如何数字化转型

半导体制造的数字化转型正通过技术融合与流程重构&#xff0c;推动着这个精密产业的全面革新。全球芯片短缺与工艺复杂度指数级增长的双重压力下&#xff0c;头部企业已构建起四大转型支柱&#xff1a; 1. 数据中枢重构产线生态 台积电的「智慧工厂4.0」部署着30万物联网传感器…...

LabVIEW 程序持续优化

LabVIEW 以其独特的图形化编程方式&#xff0c;在工业自动化、测试测量、数据分析等众多领域发挥着关键作用。为了让 LabVIEW 程序始终保持高效、稳定&#xff0c;并契合不断变化的实际需求&#xff0c;持续改进必不可少。下面将从多个关键维度&#xff0c;为大家细致地介绍通用…...

Windows10系统RabbitMQ无法访问Web端界面

项目场景&#xff1a; 提示&#xff1a;这里简述项目相关背景&#xff1a; 项目场景&#xff1a; 在一个基于 .NET 的分布式项目中&#xff0c;团队使用 RabbitMQ 作为消息队列中间件&#xff0c;负责模块间的异步通信。开发环境为 Windows 10 系统&#xff0c;开发人员按照官…...

初阶数据结构--链式二叉树

二叉树&#xff08;链式结构&#xff09; 前面的文章首先介绍了树的相关概念&#xff0c;阐述了树的存储结构是分为顺序结构和链式结构。其中顺序结构存储的方式叫做堆&#xff0c;并且对堆这个数据结构进行了模拟实现&#xff0c;并进行了相关拓展&#xff0c;接下来会针对链…...

Tree Shaking(摇树优化)详解

Tree Shaking&#xff08;摇树优化&#xff09;详解 Tree Shaking 是现代 JavaScript 打包工具&#xff08;如 Webpack、Rollup、Vite等&#xff09;中的一项重要优化技术&#xff0c;它的名字形象地比喻为"摇动一棵树&#xff0c;让没用的叶子掉下来"。 核心概念 …...

SpringAI版本更新:向量数据库不可用的解决方案!

Spring AI 前两天&#xff08;4.10 日&#xff09;更新了 1.0.0-M7 版本后&#xff0c;原来的 SimpleVectorStore 内存级别的向量数据库就不能用了&#xff0c;Spring AI 将其全部源码删除了。 此时我们就需要一种成本更低的解决方案来解决这个问题&#xff0c;如何解决呢&…...

BladeX单点登录与若依框架集成实现

1. 概述 本文档详细介绍了将BladeX认证系统与若依(RuoYi)框架集成的完整实现过程。集成采用OAuth2.0授权码流程&#xff0c;使用户能够通过BladeX账号直接登录若依系统&#xff0c;实现无缝单点登录体验。 2. 系统架构 2.1 总体架构 #mermaid-svg-YxdmBwBtzGqZHMme {font-fa…...

JVM 内存调优

内存调优 内存泄漏&#xff08;Memory Leak&#xff09;和内存溢出&#xff08;Memory Overflow&#xff09;是两种常见的内存管理问题&#xff0c;它们都可能导致程序执行不正常或系统性能下降&#xff0c;但它们的原因和表现有所不同。 内存泄漏 内存泄漏&#xff08;Memo…...

Shell脚本提交Spark任务简单案例

一、IDEA打包SparkETL模块&#xff0c;上传值HDFS的/tqdt/job目录 二、创建ods_ETL.sh脚本 mkdir -p /var/tq/sh/dwd vim /var/tq/sh/dwd/ods_ETL.sh chmod 754 /var/tq/sh/dwd/ods——ETL.sh #脚本内容如下 #!/bin/bash cur_date$(date %Y-%m-%d) /opt/bigdata/spark-3.3.2/b…...

国标GB28181视频平台EasyCVR视频汇聚系统,打造别墅居民区智能监控体系

一、现状背景 随着国家经济的快速增长&#xff0c;生活水平逐渐提高&#xff0c;私人别墅在城市、乡镇和农村的普及率也在逐年增加。然而&#xff0c;由于别墅区业主经济条件较好&#xff0c;各类不法事件也日益增多&#xff0c;主要集中在以下几个方面&#xff1a; 1&#x…...

BGP分解实验·23——BGP选路原则之路由器标识

在选路原则需要用到Router-ID做选路决策时&#xff0c;其对等体Router-ID较小的路由将被优选&#xff1b;其中&#xff0c;当路由被反射时&#xff0c;包含起源器ID属性时&#xff0c;该属性将代替router-id做比较。 实验拓扑如下&#xff1a; 实验通过调整路由器R1和R2的rout…...

机器学习(5)——支持向量机

1. 支持向量机&#xff08;SVM&#xff09;是什么&#xff1f; 支持向量机&#xff08;SVM&#xff0c;Support Vector Machine&#xff09;是一种监督学习算法&#xff0c;广泛应用于分类和回归问题&#xff0c;尤其适用于高维数据的分类。其核心思想是寻找最优分类超平面&am…...