React的HTML写法和原生JavaScript操作DOM有什么区别?

作者:交互手记

时间:2026-11-12

浏览:213

深入对比React的HTML写法(JSX)与原生JavaScript操作DOM的本质区别。从思维模型、语法机制、性能调和、事件系统及选型场景等维度进行详尽解析,配有完整架构与流程图解。

文章目录
技术对比 · 前端核心原理
许多刚接触 React 的开发者都会产生一个直观疑惑:“React 组件里写的不就是 HTML 吗?为什么不直接用原生 JS 操作 DOM?”表面上看,两者都在描述界面的结构和交互,但其实有着本质的区别:原生 JavaScript 操作 DOM 是命令式的过程,开发者需要亲自向浏览器发出“先找节点、再造元素、最后追加并挂载事件”的繁琐指令;而 React 的 HTML 写法(JSX)则是声明式的状态投影,开发者只需描述“当前数据状态下界面应当长成什么样(UI = f(state))”,底层由 React 自动完成状态到真实 DOM 的映射与最小化更新。理解这两种模式的区别,是搞懂现代前端开发心智模型的关键一步。
React的HTML写法和原生JavaScript操作DOM有什么区别?
图解概要:React JSX 与原生 DOM 操作的核心维度横向对比

一、思维模型差异:命令式“指令搬砖”与声明式“状态投影”

我们先来看一个最日常的界面需求:点击按钮,数字加 1,并在页面列表中新增一条记录。两种方式解决这个问题的思路完全不同。

1. 原生 JavaScript:把注意力放在“执行步骤”上

在没有框架的原生代码中,开发者必须事无巨细地规划每一个 DOM 操作步骤。你需要手动定位容器、创建新节点、写入文本、添加 CSS 类,再将其插入到文档树的指定位置。

// 原生 JS:开发者必须手写每一步 DOM 操作 const btn = document.getElementById('add-btn'); const list = document.getElementById('item-list'); let count = 0; btn.addEventListener('click', () => { count++; // 步骤 1:手动创建 li 节点 const li = document.createElement('li'); // 步骤 2:设置属性与内容 li.className = 'list-item'; li.textContent = `当前项目编号:${count}`; // 步骤 3:挂载单项点击事件 li.addEventListener('click', () => alert(count)); // 步骤 4:插入文档树 list.appendChild(li); });

这种做法的隐患在于:数据状态与界面展示相互分离且极易脱节。如果另一个业务逻辑(例如重置功能或网络请求返回)也要修改这个列表,开发者必须再次编写查找与清空 DOM 节点的代码。一旦某处逻辑遗漏,界面呈现的内容就会与内存中的变量产生不一致。

2. React JSX:把注意力放在“最终状态”上

而在 React 中,你不必告诉浏览器“怎么一步一步修改界面”,而是告诉 React“当列表数据为 items 时,界面应该渲染成什么结构”。数据的流向是单向且确定的。

// React:声明式状态驱动视图 import React, { useState } from 'react'; function ItemList() { const [items, setItems] = useState([]); const addItem = () => { setItems(prev => [...prev, prev.length + 1]); }; // 只声明数据与结构的映射规则,不直接触碰任何 DOM API return ( <div> <button onClick={addItem}>添加项目</button> <ul> {items.map(num => ( <li key={num} className="list-item" onClick={() => alert(num)}> 当前项目编号:{num} </li> ))} </ul> </div> ); }
React的HTML写法和原生JavaScript操作DOM有什么区别?
图 1:思维模型对比 —— 命令式 DOM 增删改 vs 声明式状态投影

在声明式模型下,状态是唯一的真实数据源(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++ 渲染对象。

React的HTML写法和原生JavaScript操作DOM有什么区别?
图 2:从 JSX 源码到浏览器真实 DOM 渲染的全流程

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 操作,而是充当了一层智能缓冲调度器。当状态发生变更时:

  1. 全在内存中比对:React 首先在内存中生成新的虚拟 DOM 树,并与旧树进行高效 Diff(基于启发式算法与唯一 key 值的 O(n) 比对)。
  2. 批量合并更新(Batching):React 会将同一周期内的多次 setState 自动合并,不立即触发浏览器绘制。
  3. 精准提交补丁(Commit):计算出真正发生改变的最小节点差异后,React 在 Commit 阶段一次性、集中地对真实 DOM 打补丁。
React的HTML写法和原生JavaScript操作DOM有什么区别?
图 3:更新机制对比 —— 原生频繁直接操作 vs React 批量调和更新
客观认知:虚拟 DOM 真的“更快”吗?
如果让一位资深工程师针对特定场景手写原生 DOM 操作(配合 DocumentFragment 和手写防抖),其极限执行耗时必然低于 React,因为原生代码省去了虚拟 DOM 对象的构建和 Diff 比对成本。

但是,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 会随生命周期自动清理关联引用,彻底免除了手动解绑监听的记忆负担。
React的HTML写法和原生JavaScript操作DOM有什么区别?
图 4:事件体系对比 —— 原生离散监听 vs 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 运行时
维护难度 状态多时极易与视图脱节,代码冗长 组件化高内聚,状态单向流动,极易维护
React的HTML写法和原生JavaScript操作DOM有什么区别?
图 5:技术选型决策图 —— 什么时候用原生 DOM?什么时候用 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 节点的引用:

import React, { useRef, useEffect } from 'react'; function AutoInput() { // 1. 创建 ref 引用容器 const inputRef = useRef(null); useEffect(() => { // 2. 在副作用中通过 ref.current 安全访问原生 DOM 节点 if (inputRef.current) { inputRef.current.focus(); // 调用原生聚焦方法 } }, []); // 3. 将 ref 绑定到 JSX 标签上 return <input ref={inputRef} placeholder="页面挂载后自动聚焦" />; }
注意事项:避免在 useRef 中手动改变 DOM 树结构
使用 useRef 时,仅执行聚焦、播放视频、获取滚动位置等只读或行为型操作。切勿通过 ref.current.appendChild() 或 ref.current.innerHTML = "..." 手动增删子节点。一旦绕过 React 强行修改底层 DOM 树,会导致 React 的内部虚拟 DOM 记录与真实 DOM 产生错位,从而引发诡异的运行时渲染崩溃。

结语

总结来说,React 的 HTML 写法并不是单纯给原生 DOM 套了一层语法外衣,而是从“手把手指导浏览器如何改动”到“以状态为核心描述最终目标”的心智升级。

当你面对的是一个简单的静态脚本,原生 DOM 操作轻便直观;而当你构建的是包含海量状态联动、多层组件嵌套与团队多人协作的复杂应用时,React 的声明式 JSX、虚拟 DOM 调和与统一合成事件,能为你筑牢数据与界面的确定性防线。

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章