React从基础入门到高级实战:React 基础入门 - React 的工作原理:虚拟 DOM 与 Diff 算法

React 的工作原理:虚拟 DOM 与 Diff 算法

引言

React 是现代前端开发的明星框架,它的出现彻底改变了我们构建用户界面的方式。无论是动态的 Web 应用还是复杂的单页应用(SPA),React 都能以高效的渲染机制和简洁的组件化开发模式让开发者受益匪浅。那么,React 为什么如此强大?答案就在于它的两大核心机制:虚拟 DOM(Virtual DOM)Diff 算法

简单来说,虚拟 DOM 是 React 的“秘密武器”,它在内存中模拟真实 DOM 的结构,让 React 能够快速计算界面变化的部分,并以最小的代价更新页面。而 Diff 算法则是 React 的“智能大脑”,它通过高效比较新旧虚拟 DOM 树,找出需要更新的地方,避免了传统 DOM 操作的低效和繁琐。

这篇文章的目标是带你从零开始,深入探索虚拟 DOM 和 Diff 算法的奥秘。我们将从基础概念讲起,逐步剖析它们的实现细节,并通过丰富的代码示例和实践案例,让你不仅理解原理,还能将知识应用到实际开发中。无论你是 React 新手,还是希望深入掌握其内部机制的开发者,这篇文章都将为你提供全面而清晰的指导。


1. 虚拟 DOM 简介

1.1 什么是虚拟 DOM?

虚拟 DOM 是 React 为了提升渲染性能而设计的一种技术。它本质上是一个 JavaScript 对象,用来描述真实 DOM 的结构和内容。与直接操作真实 DOM 不同,React 先在内存中创建一个虚拟 DOM 树,通过比较新旧虚拟 DOM 的差异,再将变化应用到真实 DOM 上。

通俗比喻
想象你在写一篇文章。如果每次修改都直接在纸上涂改,你会浪费很多时间擦掉旧内容、写上新内容,甚至可能弄乱整页纸。更好的方法是先在电脑上编辑草稿,改动满意后再打印到纸上。虚拟 DOM 就像这个“草稿”,它让 React 在内存中快速调整界面结构,最后一次性更新真实 DOM,省时又高效。

1.2 为什么需要虚拟 DOM?

在传统的 Web 开发中,开发者需要通过 JavaScript 直接操作 DOM,比如添加、删除或修改元素。然而,真实 DOM 操作非常昂贵,因为每次改动都可能触发浏览器的重排(reflow)和重绘(repaint),这些过程会消耗大量计算资源。

虚拟 DOM 的出现解决了这个问题。它的主要优势包括:

  • 性能提升:虚拟 DOM 允许 React 在内存中批量处理更新,只将必要的改动应用到真实 DOM,减少重排和重绘的次数。
  • 跨平台能力:虚拟 DOM 不仅可以渲染到浏览器的真实 DOM,还可以通过 React Native 等技术渲染到移动端或其他平台。
  • 开发体验优化:开发者无需手动管理复杂的 DOM 操作,只需关注组件的状态和属性(Props),React 会自动完成渲染工作。

1.3 虚拟 DOM 的结构

虚拟 DOM 是一个树形结构,每个节点是一个 JavaScript 对象,包含以下核心属性:

  • type:节点的类型,可以是 HTML 标签(如 'div''span')或自定义 React 组件。
  • props:节点的属性,比如 classNamestyle 或事件监听器。
  • children:子节点,可以是单个节点、节点数组或纯文本。

示例

{type: 'div',props: { className: 'container', id: 'main' },children: [{ type: 'h1', props: { children: 'Welcome to React' } },{ type: 'p', props: { children: 'Learn about Virtual DOM' } }]
}

这个虚拟 DOM 表示一个 <div> 元素,包含一个 <h1> 和一个 <p> 子元素。React 正是通过这样的对象结构来描述界面的。


2. 虚拟 DOM 的工作流程

2.1 虚拟 DOM 的创建

在 React 中,虚拟 DOM 的创建始于 JSX。JSX 是一种类似 HTML 的语法糖,开发者用它来描述组件的结构,最终会被编译为 React.createElement 函数调用,生成虚拟 DOM 对象。

示例

function App() {return (<div className="app"><h1>Hello, React!</h1></div>);
}

编译后:

function App() {return React.createElement('div',{ className: 'app' },React.createElement('h1', null, 'Hello, React!'));
}

React.createElement 返回的就是一个虚拟 DOM 对象,描述了组件的层级结构。

2.2 虚拟 DOM 的更新

当组件的状态(state)或属性(props)发生变化时,React 会重新调用组件的渲染函数,生成一个新的虚拟 DOM 树。然后,React 会将新树与旧树进行比较,找出差异,再将这些差异应用到真实 DOM 上。

更新流程

  1. 状态或属性变化:比如用户点击按钮,触发 setState
  2. 生成新虚拟 DOM:React 重新渲染组件,生成新的虚拟 DOM 树。
  3. 差异比较:通过 Diff 算法,React 计算新旧虚拟 DOM 的差异。
  4. 更新真实 DOM:将差异批量应用到真实 DOM,完成界面更新。

这个过程的核心在于“比较”和“更新”的高效性,而这正是 Diff 算法的舞台。


3. Diff 算法详解

Diff 算法是 React 的核心优化机制,它负责比较新旧虚拟 DOM 树,找出最小的更新操作。React 的 Diff 算法基于以下三个假设和策略:

  1. 分层比较(Tree Diff):只比较同一层级的节点,跨层级操作较少。
  2. 组件类型比较(Component Diff):相同类型的组件可以复用,不同类型则销毁重建。
  3. 同层元素比较(Element Diff):通过 key 属性优化同层节点的匹配效率。

图解 Diff 算法

旧虚拟 DOM       新虚拟 DOMA                A/ \              / \
B   C            B   D|                |E                F
  • A 节点相同,复用。
  • C 节点类型不同,替换为 D。
  • E 节点替换为 F。

下面我们逐一深入探讨这三个阶段。

3.1 Tree Diff:分层比较

React 的 Diff 算法首先从树的根节点开始,逐层比较新旧虚拟 DOM。如果某层的节点类型不同,React 会直接删除旧节点及其所有子节点,然后用新节点替换。这种策略基于一个假设:不同类型的节点通常会生成完全不同的 DOM 结构,继续比较子节点没有意义。

示例

// 旧虚拟 DOM
{type: 'div',props: { className: 'box' },children: [{ type: 'span', props: { children: 'Text' } }]
}// 新虚拟 DOM
{type: 'section',props: { className: 'box' },children: [{ type: 'p', props: { children: 'Text' } }]
}
  • 根节点类型从 'div' 变为 'section'
  • React 删除旧的 <div> 及其 <span> 子节点,创建新的 <section><p>

优点:分层比较避免了对整棵树的逐一遍历,极大提高了效率。

3.2 Component Diff:组件类型比较

在比较同一层级的节点时,如果节点是一个 React 组件,React 会先检查组件的类型:

  • 相同类型组件:React 继续比较其 propsstate,更新内部状态并复用实例。
  • 不同类型组件:React 销毁旧组件实例(包括其子树),创建新组件实例。

示例

// 旧渲染
function OldComponent() {return <div>Old</div>;
}
<OldComponent />// 新渲染
function NewComponent() {return <div>New</div>;
}
<NewComponent />
  • 组件类型不同,React 销毁 OldComponent,创建 NewComponent

注意:即使两个组件渲染的 DOM 结构相同,类型不同也会触发重建,因此尽量保持组件类型的稳定性。

3.3 Element Diff:同层元素比较

对于同一层级的普通元素(如 <li><div>),React 会逐一比较它们的类型和属性。如果是列表渲染,React 还会利用 key 属性来优化比较效率。

Element Diff 的三种操作

  • 插入:新树中有旧树中没有的节点。
  • 删除:旧树中有新树中没有的节点。
  • 移动:节点在新旧树中都存在,但位置不同。

key 的作用
key 是 React 识别节点的唯一标识,帮助 Diff 算法快速匹配新旧节点。没有 key 时,React 按顺序比较,效率低下;有了 key,React 能准确判断节点的移动、插入和删除。

示例

// 旧列表
<ul><li key="a">A</li><li key="b">B</li><li key="c">C</li>
</ul>// 新列表
<ul><li key="a">A</li><li key="d">D</li><li key="b">B</li>
</ul>
  • key="a" 的节点不变。
  • key="c" 的节点被删除。
  • key="d" 的节点被插入。
  • key="b" 的节点移动到新位置。

性能对比

  • key:React 按顺序逐一比较,可能导致所有节点重建。
  • key:React 只更新必要的部分,减少 DOM 操作。

4. 虚拟 DOM 与真实 DOM 的关系

虚拟 DOM 是 React 的“中间人”,它在内存中模拟真实 DOM 的结构和变化。React 的渲染流程可以分为以下几步:

  1. 初次渲染:根据初始虚拟 DOM 树创建真实 DOM。
  2. 状态变化:生成新的虚拟 DOM 树。
  3. Diff 比较:计算新旧虚拟 DOM 的差异。
  4. 批量更新:将差异应用到真实 DOM。

性能优势

  • 内存操作比真实 DOM 操作快得多。
  • 批量更新减少了浏览器重排和重绘的频率。

图解(文字描述):
想象两棵树:旧虚拟 DOM 和新虚拟 DOM。React 用 Diff 算法“剪枝”,只保留需要更新的部分,然后将这些“剪枝”结果同步到真实 DOM 上。


5. 实践案例:计数器应用

让我们通过一个简单的计数器应用,观察虚拟 DOM 和 Diff 算法的实际工作过程。

以下是完整的代码:

<!DOCTYPE html>
<html lang="zh-CN">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>React 计数器</title><script src="https://cdn.jsdelivr.net/npm/react@18.2.0/umd/react.development.js"></script><script src="https://cdn.jsdelivr.net/npm/react-dom@18.2.0/umd/react-dom.development.js"></script><script src="https://cdn.tailwindcss.com"></script>
</head>
<body><div id="root" class="p-8 bg-gray-100 min-h-screen flex items-center justify-center"></div><script type="text/babel">function Counter() {const [count, setCount] = React.useState(0);console.log('渲染 Counter 组件,新 count 值:', count);return (<div className="bg-white p-6 rounded-lg shadow-lg text-center"><h1 className="text-3xl font-bold mb-4">计数器</h1><p className="text-xl mb-6">当前计数: <span className="font-semibold">{count}</span></p><buttononClick={() => setCount(count + 1)}className="px-6 py-2 bg-blue-500 text-white rounded-lg hover:bg-blue-600 transition">增加</button></div>);}const root = ReactDOM.createRoot(document.getElementById('root'));root.render(<Counter />);</script>
</body>
</html>

运行步骤

  1. 将代码保存为 index.html 文件。
  2. 用浏览器打开,点击“增加”按钮。
  3. 打开开发者工具(F12),在控制台观察每次渲染的日志。

观察虚拟 DOM 更新

  • 初次渲染:React 创建初始虚拟 DOM,渲染为真实 DOM,显示 count: 0
  • 点击按钮setCount 触发状态更新,React 生成新虚拟 DOM。
  • Diff 过程:React 比较新旧虚拟 DOM,发现只有 <span> 的文本从 0 变为 1
  • 真实 DOM 更新:React 只更新 <span> 的内容,其他部分保持不变。

通过这个案例,你可以看到虚拟 DOM 和 Diff 算法如何高效地处理局部更新,避免了整棵 DOM 树的重建。


6. 性能优化技巧

虚拟 DOM 和 Diff 算法为 React 提供了良好的性能基础,但开发者仍需掌握一些优化技巧,以进一步提升应用效率。

6.1 正确使用 key

在列表渲染中,始终为每个元素提供稳定且唯一的 key。避免使用数组索引作为 key,因为索引会随列表变化而改变,可能导致 Diff 算法误判。

错误示例

items.map((item, index) => <li key={index}>{item}</li>)

正确示例

items.map((item) => <li key={item.id}>{item.name}</li>)

6.2 避免不必要渲染

使用 React.memoshouldComponentUpdate 防止组件在 props 或 state 未改变时重渲染。

示例

const Child = React.memo(function Child({ value }) {console.log('Child 渲染');return <div>{value}</div>;
});

6.3 按需加载组件

使用 React.lazySuspense 实现组件的动态加载,减少初始加载时间。

示例

const LazyComponent = React.lazy(() => import('./LazyComponent'));function App() {return (<Suspense fallback={<div>加载中...</div>}><LazyComponent /></Suspense>);
}

7. 进阶内容:React 19 新特性

React 19 引入了一些令人兴奋的新特性,进一步优化了虚拟 DOM 和 Diff 算法的性能。

7.1 并发渲染

并发渲染(Concurrent Rendering)允许 React 在渲染过程中暂停和恢复任务,提高应用的响应性。比如,当用户输入时,React 可以优先处理输入事件,再继续渲染其他部分。

7.2 Server Components

Server Components 将部分组件逻辑移到服务器执行,客户端只接收渲染结果。这减少了客户端的计算负担,同时保留了虚拟 DOM 的高效更新能力。

影响

  • 虚拟 DOM 的生成和 Diff 过程可能部分发生在服务器端。
  • 客户端只需处理少量动态更新,进一步提升性能。

8. 总结与展望

虚拟 DOM 和 Diff 算法是 React 高效渲染的基石。通过在内存中模拟真实 DOM,React 能够快速比较和计算界面变化;借助 Diff 算法的分层比较和 key 优化,React 确保了更新的高效性和准确性。

掌握这些原理不仅能让你更好地理解 React,还能帮助你在开发中应用性能优化技巧,比如合理使用 key、避免不必要渲染等。随着 React 19 的到来,虚拟 DOM 和 Diff 算法的潜力将被进一步挖掘,值得每位开发者持续关注。

希望这篇文章能让你对 React 的核心机制有全面而深入的理解!如果有任何疑问,欢迎随时交流。

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

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

相关文章

解释一下NGINX的反向代理和正向代理的区别?

大家好&#xff0c;我是锋哥。今天分享关于【解释一下NGINX的反向代理和正向代理的区别?】面试题。希望对大家有帮助&#xff1b; 解释一下NGINX的反向代理和正向代理的区别? NGINX的反向代理和正向代理的区别主要体现在它们的功能和使用场景上。下面我会详细解释它们的定义…

Python学习——执行python时,键盘按下ctrl+c,退出程序

在 Python 中&#xff0c;当用户按下 CtrlC 时&#xff0c;程序默认会触发 KeyboardInterrupt 异常并终止。 1. 捕获 KeyboardInterrupt 异常&#xff08;推荐&#xff09; 使用 try-except 块直接捕获 KeyboardInterrupt 异常&#xff0c;适用于简单场景。 示例代码&#xff…

C++ 反向迭代器(Reverse Iterator)实现详解

目录 1. 反向迭代器概述 2. 代码实现分析 3. 关键点解析 3.1 模板参数设计 3.2 核心操作实现 4. 使用示例 1. 反向迭代器概述 反向迭代器是STL中一种重要的适配器&#xff0c;它允许我们以相反的顺序遍历容器。本文将详细讲解如何实现一个自定义的反向迭代器模板类。 2.…

动态DNS管理:【etcd+CoreDNS】 vs【BIND9】便捷性对比

对比 BIND9 集群和 etcdCoreDNS 集群在便捷性方面&#xff0c;通常情况下&#xff0c;对于需要动态、频繁变更 DNS 记录以及追求云原生和自动化集成的场景&#xff0c;etcdCoreDNS 方案更加便捷。 然而&#xff0c;“便捷性”也取决于具体的应用场景、团队的技术栈和运维习惯。…

基于大模型的短暂性脑缺血发作预测与干预全流程系统技术方案大纲

目录 一、系统概述二、系统架构(一)数据采集层(二)大模型核心层(三)应用服务层(四)数据存储与管理层三、全流程技术方案(一)术前阶段(二)术中阶段(三)术后阶段(四)并发症风险预测(五)手术方案制定(六)麻醉方案制定(七)术后护理(八)统计分析(九)技术验…

MSP430通用电机控制代码(Motor)设计与实现

一、代码结构概览 // Motor.h // Motor.h #ifndef __MOTOR_H_ #define __MOTOR_H_#include "A_include.h"void Motor_Init(void); // 初始化函数 void PWM_SET(int duty0, int duty1); // PWM设置函数#endif// Motor.c // Motor.c #include "Motor.h"…

25年软考架构师真题(回忆更新中)

论文题: 系统负载均衡设计方法事件驱动架构多模型数据库应用软件测试架构案例分析: 必选题:1.1填写质量属性的质量属性名 1.2解释器风格架构的组成图填空,以及解释为什么该模型适用解释器风格 选做题1redis2.1全量复制的流程图 <

优化用户体验:拦截浏览器前进后退、刷新、关闭、路由跳转等用户行为并弹窗提示

&#x1f9d1;‍&#x1f4bb; 写在开头 点赞 收藏 学会&#x1f923;&#x1f923;&#x1f923; 需求 首先列举一下需要拦截的行为&#xff0c;接下来我们逐个实现。 浏览器前进后退标签页刷新和关闭路由跳转 1、拦截浏览器前进后退 这里的实现是核心&#xff0c;涉及到大…

Docker:容器化技术

引言 传统部署环境逐渐不适应现在的企业开发&#xff0c;为了追求更加轻量&#xff0c;更加容易管理项目&#xff0c;引入了docker容器化技术去实现更加高效的部署环境。 一.docker风光下的内核功能和常用命令 1.docker容器和虚拟机的区别 我们在底层和应用层之间引入了一层do…

ping命令常用参数以及traceout命令

在网络故障排查和性能分析中&#xff0c;ping和 traceroute&#xff08;Windows中通常称为 tracert&#xff09;是两个极为重要的工具。它们帮助诊断网络连接问题&#xff0c;了解数据在网络中的传输路径。下面将详细介绍这两个命令的常用参数及其应用。 ping命令 ping命令用…

SpringBoot开发——Spring Boot异常处理全攻略:五大方案实战对比

文章目录 一、血泪教训:异常处理的代价二、五大异常处理方案详解2.1 全局异常处理(推荐方案)2.2 控制器级处理2.3 HTTP状态码注解2.4 ResponseEntity精细控制2.5 自定义异常体系(企业级方案)三、五大方案对比决策表四、四大避坑指南4.1 异常吞噬陷阱4.2 循环依赖问题4.3 异…

CodeBuddy 实现图片转素描手绘工具

本文所使用的 CodeBuddy 免费下载链接&#xff1a;腾讯云代码助手 CodeBuddy - AI 时代的智能编程伙伴 前言 最近在社交媒体上&#xff0c;各种素描风格的图片火得一塌糊涂&#xff0c;身边不少朋友都在分享自己的 “素描照”&#xff0c;看着那些黑白线条勾勒出的独特韵味&a…

2025.05.21华为暑期实习机考真题解析第二题

📌 点击直达笔试专栏 👉《大厂笔试突围》 💻 春秋招笔试突围在线OJ 👉 笔试突围OJ 02. 灾区物资调度路径规划 问题描述 在一次严重的自然灾害后,LYA负责协调救援物资的配送工作。救援区域包含多个受灾乡镇和一个物资集结点,各个地点之间的道路状况各异,有些甚至…

Gartner《Optimize GenAI Strategy for 4 Key ConsumerMindsets》学习心得

一、引言 在当今数字化营销浪潮中,生成式人工智能(GenAI)正以前所未有的速度重塑着市场格局。GenAI 既是一场充满机遇的变革,也是一场潜在风险的挑战。一方面,绝大多数 B2C 营销领导者对 GenAI 赋能营销抱有极高期待,他们看到了 GenAI 在提升时间与成本效率方面的巨大潜…

探索链表的奇妙世界:从基础到高级应用

链表是计算机科学中一种基础且重要的数据结构&#xff0c;它如同一条由珠子串成的项链&#xff0c;每个珠子&#xff08;节点&#xff09;都包含着数据和指向下一个珠子的线索。 与数组相比&#xff0c;链表在插入和删除操作上更加灵活&#xff0c;无需预先分配固定大小的内存…

黑马点评双拦截器和Threadlocal实现原理

文章目录 双拦截器ThreadLocal实现原理 双拦截器 实现登录状态刷新的原因&#xff1a; ​ 防止用户会话过期&#xff1a;通过动态刷新Token有效期&#xff0c;确保活跃用户不会因固定过期时间而被强制登出 ​ 提升用户体验&#xff1a;用户无需频繁重新登录&#xff0c;只要…

Windows 中动态库.dll 的 .lib 文件有什么作用?

在 Windows 平台开发中, 动态链接库(Dynamic Link Library, DLL)。与之相关的还有一个常让人困惑的文件——.lib 文件。那么,这个 .lib 文件到底有什么作用呢? 一、什么是 .lib 文件? .lib 文件是 静态导入库(Import Library) 文件,它通常与动态链接库(DLL)一起生成…

细说STM32单片机FreeRTOS消息缓冲区及其应用实例

目录 一、消息缓冲区功能概述 二、消息缓冲区操作相关函数 1、相关函数概述 2、部分函数详解 &#xff08;1&#xff09;创建消息缓冲区 &#xff08;2&#xff09;写入消息 &#xff08;3&#xff09;读取消息 &#xff08;4&#xff09;消息缓冲区状态查询 三、消息…

【缓存】JAVA本地缓存推荐Caffeine和Guava

&#x1f31f; 引言 在软件开发过程中&#xff0c;缓存是提升系统性能的常用手段。对于基础场景&#xff0c;直接使用 Java集合框架&#xff08;如Map/Set/List&#xff09;即可满足需求。然而&#xff0c;当面对更复杂的缓存场景时&#xff1a; 需要支持多种过期策略&#x…

IDA插件 MIPSROP的安装和使用方法

前言 笔者的IDA版本为9.0&#xff0c;刚开始根据一些博客描述以为将mipsrop.py拷贝到IDA的plugins目录即可&#xff0c;可操作后发现事情好像没这么简单&#xff0c;复制进去后就发现没有博客中所说的 MIPS ROP Finder &#xff0c;笔者在网上搜索了很多博客后在 https://bbs.…