React的HTML写法和原生JavaScript操作DOM有什么区别?
深入对比React的HTML写法(JSX)与原生JavaScript操作DOM的本质区别。从思维模型、语法机制、性能调和、事件系统及选型场景等维度进行详尽解析,配有完整架构与流程图解。
一、思维模型差异:命令式“指令搬砖”与声明式“状态投影”
我们先来看一个最日常的界面需求:点击按钮,数字加 1,并在页面列表中新增一条记录。两种方式解决这个问题的思路完全不同。
1. 原生 JavaScript:把注意力放在“执行步骤”上
在没有框架的原生代码中,开发者必须事无巨细地规划每一个 DOM 操作步骤。你需要手动定位容器、创建新节点、写入文本、添加 CSS 类,再将其插入到文档树的指定位置。
这种做法的隐患在于:数据状态与界面展示相互分离且极易脱节。如果另一个业务逻辑(例如重置功能或网络请求返回)也要修改这个列表,开发者必须再次编写查找与清空 DOM 节点的代码。一旦某处逻辑遗漏,界面呈现的内容就会与内存中的变量产生不一致。
2. React JSX:把注意力放在“最终状态”上
而在 React 中,你不必告诉浏览器“怎么一步一步修改界面”,而是告诉 React“当列表数据为 items 时,界面应该渲染成什么结构”。数据的流向是单向且确定的。
在声明式模型下,状态是唯一的真实数据源(Single Source of Truth)。界面只是状态在某一时刻的投影。只要状态变了,界面就会自动跟着同步,开发者永远不需要去写 getElementById 或 appendChild。
二、语法与底层本质:真实 HTML 节点与虚拟 React 元素
初学者最容易误解的地方,就是以为在 JSX 中写的 <div>、<span> 就是真实的 HTML 字符串。实际上,JSX 根本不是 HTML,而是 JavaScript 的语法糖。
1. 编译阶段的本质转化
浏览器引擎只认识 HTML、CSS 和纯 JavaScript,并不能直接解析 JSX。在项目构建时,Babel 或 SWC 等编译器会将所有的 JSX 语法转化为普通的 JavaScript 函数调用:
- 旧版编译:转化为
React.createElement(type, props, ...children) - React 17+ 新转换:转化为
_jsx("div", { className: "box", children: ... })
这意味着,你在代码中写的每一个看似 HTML 的标签,运行后得到的都是一个轻量的普通 JavaScript 对象(即 Virtual DOM / Fiber 节点),它仅仅记录了节点的类型、属性和子节点信息,并不对应任何浏览器的原生 C++ 渲染对象。
2. 属性写法上的核心差异
正因为 JSX 是在 JavaScript 环境中评估的表达式,为了避免与 JavaScript 的保留关键字冲突,并保持驼峰命名一致性,React 制定了一系列与原生 HTML 不同的属性规则:
-
class 与 className:在原生 HTML 中使用
class="title";但在 JSX 中,由于class是 JS 的保留关键字,必须改写为className="title"。 -
for 与 htmlFor:原生表单的
<label for="user">在 JSX 中必须写为<label htmlFor="user">。 -
行内样式(style):原生 HTML 的 style 是字符串
style="color: red; margin-top: 8px;";而 JSX 的 style 必须是一个 JavaScript 对象,采用驼峰命名法:style={{ color: 'red', marginTop: '8px' }}。这不仅避免了字符串拼接错误,还能享受编辑器的类型提示。 -
布尔属性与表达式插值:原生 HTML 拼接动态属性必须使用模板字符串;JSX 中则使用大括号
{expression}随意嵌入任意合法的 JavaScript 表达式,包括三元运算、函数调用和变量。
三、渲染性能与更新策略:频繁直接触碰 vs 虚拟 DOM 调和
很多文章会宣称“React 虚拟 DOM 比原生 DOM 速度更快”,这个说法并不严谨,容易让初学者产生误解。我们需要看清两者的底层性能机制。
1. 原生 DOM 操作的性能开销来源
DOM 是由浏览器内核暴露出来的接口,JavaScript 引擎与渲染引擎相互独立。每次通过原生 JS 访问或修改 DOM,都会产生跨模块调用的开销。
更关键的是:如果代码中频繁、分散地读取和修改 DOM 属性(例如在循环中连续修改样式或文本),会强行打断浏览器的内部优化队列,造成强制同步布局与布局抖动(Layout Thrashing),引发连续的重排(Reflow)与重绘(Repaint),最终导致界面掉帧。
2. React 的虚拟 DOM 与 Fiber 调和机制
React 并不是去消灭 DOM 操作,而是充当了一层智能缓冲调度器。当状态发生变更时:
- 全在内存中比对:React 首先在内存中生成新的虚拟 DOM 树,并与旧树进行高效 Diff(基于启发式算法与唯一
key值的 O(n) 比对)。 - 批量合并更新(Batching):React 会将同一周期内的多次
setState自动合并,不立即触发浏览器绘制。 - 精准提交补丁(Commit):计算出真正发生改变的最小节点差异后,React 在 Commit 阶段一次性、集中地对真实 DOM 打补丁。
但是,React 带来的核心价值是“免于人工微调的高性能下限”。在业务逻辑高度复杂、多人协作的大型项目中,普通开发者手写原生 DOM 极易写出低效或有缺陷的代码;而 React 保证了无论业务多么复杂,只要按规范写状态,应用都能自动获得稳定、批量的最优更新体验。
四、事件体系与内存管控:分散监听与合成事件(SyntheticEvent)
处理用户点击、输入等交互时,React 的写法和原生 JS 也存在深层不同。
1. 原生 DOM 事件的绑定与内存隐患
在原生 JS 中,常见的做法是在每个目标元素上单独调用 addEventListener。如果列表有 1000 个选项,就会创建 1000 个独立的事件监听器对象,占用可观的内存。
更严重的是内存回收问题:当一个带有监听器的 DOM 节点被 innerHTML = "" 直接从文档中移除时,如果不显式执行 removeEventListener,旧的事件函数如果闭包引用了上下文对象,就会导致垃圾回收器无法释放内存,长期运行的单页应用很容易因此出现内存泄漏。
2. React 的合成事件机制(SyntheticEvent)
在 React 中,你在 JSX 属性上声明的 onClick={{handleClick}},实际上并没有直接绑定到那个真实的 HTML 元素上。
- 全自动事件委托(Event Delegation):React 17+ 会统一将所有事件监听器绑定在 React 应用的根容器节点(如 #root)上。子节点触发的原生事件冒泡到根节点后,由 React 统一捕获分发。
- 跨浏览器抹平:React 将原生浏览器事件包装为一个标准的
SyntheticEvent对象,统一了不同浏览器在事件冒泡、阻止默认行为(e.preventDefault())等方面的 API 差异。 - 声明式绑定与自动回收:由于事件处理函数保存在组件 Fiber 节点的 props 中,当组件卸载或节点移除时,React 会随生命周期自动清理关联引用,彻底免除了手动解绑监听的记忆负担。
五、特性对比与技术选型:何时使用原生 DOM?
为了在实际开发中做出合理决策,我们先将 React JSX 与原生 DOM 操作的核心差异归纳为对比矩阵:
| 对比维度 | 原生 JavaScript 操作 DOM | React 的 HTML 写法 (JSX) |
|---|---|---|
| 思维模式 | 命令式(一步步指示如何改变 DOM) | 声明式(通过状态描述界面最终结构) |
| 底层实质 | 直接调用浏览器 C++ 原生 DOM 接口 | 编译为普通 JS 函数,运行生成虚拟 DOM |
| 属性规范 | 原生 HTML 标准(class, for, 字符串 style) | 驼峰命名规范(className, htmlFor, 对象 style) |
| 更新机制 | 直接修改;频繁写引发重排重绘 | Fiber Diff 调和计算,统一批量 Commit 补丁 |
| 事件系统 | 每个元素直接绑定原生 Event,需手动清理 | 根节点事件委托,自动封装 SyntheticEvent |
| 工程依赖 | 零依赖,浏览器直接运行,无需构建工具 | 需要 Babel/SWC 编译工具链与 React 运行时 |
| 维护难度 | 状态多时极易与视图脱节,代码冗长 | 组件化高内聚,状态单向流动,极易维护 |
1. 什么时候应该坚守原生 DOM 操作?
框架并不是银弹,在以下场景中,直接使用原生 JavaScript 操作 DOM 是更务实、高效的选择:
- 静态内容为主的页面或活动页:仅需要少量的动效交互(如点击展开菜单、打开弹窗)。引入体积数十 KB 的框架和整套构建流程属于过度设计。
- 独立的轻量微部件 / 浏览器扩展脚本:如用户油猴脚本、嵌在第三方博客的分享微部件,要求零外部依赖且加载即运行。
- 纯图形渲染控制:直接使用 HTML5 Canvas、WebGL 或 Three.js 进行连续帧绘制时,图元不是常规 DOM,不需要虚拟 DOM 介入。
2. 在 React 中确实需要操作原生 DOM 怎么办?
在实际业务中,我们有时确实需要直接获取原生 DOM 节点(例如:自动聚焦输入框、测量元素的 getBoundingClientRect 尺寸、或者将 DOM 容器传递给非 React 图表库如 ECharts)。
React 提供了官方的“逃生舱”——useRef 机制。通过 useRef,你可以在保留 React 声明式状态体系的同时,安全获取原生 DOM 节点的引用:
useRef 时,仅执行聚焦、播放视频、获取滚动位置等只读或行为型操作。切勿通过 ref.current.appendChild() 或 ref.current.innerHTML = "..." 手动增删子节点。一旦绕过 React 强行修改底层 DOM 树,会导致 React 的内部虚拟 DOM 记录与真实 DOM 产生错位,从而引发诡异的运行时渲染崩溃。
结语
总结来说,React 的 HTML 写法并不是单纯给原生 DOM 套了一层语法外衣,而是从“手把手指导浏览器如何改动”到“以状态为核心描述最终目标”的心智升级。
当你面对的是一个简单的静态脚本,原生 DOM 操作轻便直观;而当你构建的是包含海量状态联动、多层组件嵌套与团队多人协作的复杂应用时,React 的声明式 JSX、虚拟 DOM 调和与统一合成事件,能为你筑牢数据与界面的确定性防线。
Slack旧浏览器为何无法登录?11月支持期限与日本AI路由变化
2026-11-10 19:00:14
网页提示请启用JavaScript但设置已开启怎么办
2026-11-06 22:02:50
启用JavaScript会影响隐私安全吗?浏览器脚本设置建议
2026-11-06 18:57:39
HTML网页中引入JavaScript的三种方式及执行顺序
2026-11-06 11:24:43
支持HTML/CSS/JS混合编写的在线IDE有哪些
2026-11-05 22:06:32








