从 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 链表、调度器、异步渲染栈式递归、响应式依赖跟踪

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/diannao/79280.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用Redis实现分布式限流

一、限流场景与算法选择 1.1 为什么需要分布式限流 在高并发系统中,API接口的突发流量可能导致服务雪崩。传统的单机限流方案在分布式环境下存在局限,需要借助Redis等中间件实现集群级流量控制。 1.2 令牌桶算法优势 允许突发流量:稳定速…

快速搭建WordPress网站的主题

WP快主题(wpkuai.com )是一款由知名WordPress专业团队打造的专业化WordPress主题,旨在让用户使用该wordpress主题快速搭建网站。 WP快主题专注于快速搭建WordPress网站的主题解决方案。其主题设计注重简洁性与高效性,旨在帮助用户快速完成网站的搭建和部…

STM32江科大----------PID算法

声明:本人跟随b站江科大学习,本文章是观看完视频后的一些个人总结和经验分享,也同时为了方便日后的复习,如果有错误请各位大佬指出,如果对你有帮助可以点个赞小小鼓励一下,本文章建议配合原视频使用❤️ 如…

将JSON格式的SQL查询转换为完整SQL语句的实战解析

一、背景与需求 在现代数据处理中,JSON格式因其灵活性和可读性,常被用于定义SQL查询的结构。然而,直接编写JSON格式的SQL指令后,如何将其转换为可执行的SQL语句是开发者常遇到的挑战。本文将通过一个Python函数和多个实际案例,解析如何将JSON结构转换为完整的SQL语句,并…

java CountDownLatch用法简介

CountDownLatch倒计数锁存器 CountDownLatch:用于协同控制一个或多个线程等待在其他线程中执行的一组操作完成,然后再继续执行 CountDownLatch用法 构造方法:CountDownLatch(int count),count指定等待的条件数(任务…

Leetcode - 双周赛135

目录 一、3512. 使数组和能被 K 整除的最少操作次数二、3513. 不同 XOR 三元组的数目 I三、3514. 不同 XOR 三元组的数目 II四、3515. 带权树中的最短路径 一、3512. 使数组和能被 K 整除的最少操作次数 题目链接 本题实际上求的就是数组 nums 和的余数,代码如下&…

【后端】【python】利用反射器----动态设置装饰器

📘 Python 装饰器进阶指南 一、装饰器本质 ✅ 本质概念 Python 装饰器的本质是 函数嵌套 返回函数,它是对已有函数的增强,不修改原函数代码,使用语法糖 decorator 实现包裹效果。 def my_decorator(func):def wrapper(*args, …

Nodejs Express框架

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

Docker Swarm 集群

Docker Swarm 集群 本文档介绍了 Docker Swarm 集群的基本概念、工作原理以及相关命令使用示例,包括如何在服务调度中使用自定义标签。本文档适用于需要管理和扩展 Docker 容器化应用程序的生产环境场景。 1. 什么是 Docker Swarm Docker Swarm 是用于管理 Docker…

充电宝项目中的MQTT(轻量高效的物联网通信协议)

文章目录 补充:HTTP协议MQTT协议MQTT的核心特性MQTT vs HTTP:关键对比 EMQX项目集成EMQX集成配置客户端和回调方法具体接口和方法处理处理类 补充:HTTP协议 HTTP是一种应用层协议,使用TCP作为传输层协议,默认端口是80…

【iOS】UIPageViewController学习

UIPageViewController学习 前言创建一个UIPageViewController最简单的使用 UIPageViewController的方法说明:效果展示 UIPageViewController的协议方法 前言 笔者最近在写项目时想实现一个翻书效果,上网学习到了UIPageViewController今天写本篇博客总结…

Linux搭建环境:从零开始掌握基础操作(四)

​ ​ 您好,我是程序员小羊! 前言 软件测试第一步就是搭建测试环境,如何搭建好测试环境,需要具备两项的基础知识: 1、Linux 命令: 软件测试第一个任务, 一般都需要进行环境搭建, 一部分,环境搭建内容是在服…

一天一个java知识点----Tomcat与Servlet

认识BS架构 静态资源:服务器上存储的不会改变的数据,通常不会根据用户的请求而变化。比如:HTML、CSS、JS、图片、视频等(负责页面展示) 动态资源:服务器端根据用户请求和其他数据动态生成的,内容可能会在每次请求时都…

YOLOV8 OBB 海思3516训练流程

YOLOV8 OBB 海思3516训练流程 目录 1、 下载带GPU版本的torch(可选) 1 2、 安装 ultralytics 2 3、 下载pycharm 社区版 2 4、安装pycharm 3 5、新建pycharm 工程 3 6、 添加conda 环境 4 7、 训练代码 5 9、配置Ymal 文件 6 10、修改网络结构 9 11、运行train.py 开始训练模…

【深度学习】花书第18章——配分函数

直面配分函数 许多概率模型(通常是无向图模型)由一个未归一化的概率分布 p ~ ( x , θ ) \tilde p(\mathbf x,\theta) p~​(x,θ)定义。我们必须通过除以配分函数 Z ( θ ) Z(\pmb{ \theta}) Z(θ)来归一化 p ~ \tilde p p~​。以获得一个有效的概率分…

工作记录1

日常总结、灵感记录、学习要点。持续记录 学海无涯,再好的记性也比不过烂笔头,记录一下学习日常、灵感、要点。 前言:最近看见一个博文,很有感触,是某个大佬自己运营的网站,分享了他的各种经验文章和自身的一些笔记。本人还没有他这么屌,所以还是先在CSDN上小试牛刀吧…

Spring Boot(二十一):RedisTemplate的String和Hash类型操作

RedisTemplate和StringRedisTemplate的系列文章详见: Spring Boot(十七):集成和使用Redis Spring Boot(十八):RedisTemplate和StringRedisTemplate Spring Boot(十九)…

智能指针之设计模式1

本文探讨一下智能指针和GOF设计模式的关系,如果按照设计模式的背后思想来分析,可以发现围绕智能指针的设计和实现有设计模式的一些思想体现。当然,它们也不是严格意义上面向对象的设计模式,毕竟它们没有那么分明的类层次体系&…

中间件--ClickHouse-1--基础介绍(列式存储,MPP架构,分布式计算,SQL支持,向量化执行,亿万级数据秒级查询)

1、概述 ClickHouse是一个用于联机分析(OLAP)的列式数据库管理系统(DBMS)。它由俄罗斯的互联网巨头Yandex为解决其内部数据分析需求而开发,并于2016年开源。专为大规模数据分析,实时数据分析和复杂查询设计,具有高性能、实时数据和可扩展性等…

Go之Slice和数组:深入理解底层设计与最佳实践

在Go语言中,数组(Array)和切片(Slice)是两种看似相似却本质不同的数据结构。本文将深入剖析它们的底层实现机制,并结合实际代码示例,帮助开发者掌握核心差异和使用场景。 一、基础概念&#xff…