1. 项目概述:用 JSX 写 React 元素,到底在写什么?
“Como criar elementos React com o JSX” 是葡萄牙语,直译为“如何使用 JSX 创建 React 元素”。这看似是一句基础语法教学的标题,但背后藏着前端开发中一个关键的认知分水岭:
JSX 不是 HTML,也不是模板字符串,而是一种语法糖,它最终会被编译成标准的 JavaScript 函数调用——
React.createElement()
。我带过不少刚转前端的后端工程师,他们第一次写
<div className="card">Hello</div>
时,下意识会去查“JSX 的 div 标签有哪些属性”,结果发现
class
要写成
className
、
for
得改成
htmlFor
、事件名全小写变驼峰(
onclick
→
onClick
)……当场懵住。这不是 React 故意设门槛,而是因为 JSX 本质是 JavaScript 的扩展,它必须遵守 JS 的语法规则和运行时约束。你写的每一行 JSX,都会被 Babel 或 TypeScript 编译器翻译成形如
React.createElement('div', { className: 'card' }, 'Hello')
的调用。这意味着:你不是在写“网页片段”,而是在用一种更直观的写法,构造 React 的虚拟 DOM 节点对象。这个认知一旦建立,后续学组件、props、状态、Hooks 就有了稳固的地基。本文面向三类人:零基础想入门前端的新手、写过 Vue/Angular 但对 React 渲染机制模糊的转岗者、以及准备 React 面试却总卡在“JSX 和 HTML 到底差在哪”这类问题的求职者。我会从编译原理讲到真实代码现场,不堆概念,只讲你打开编辑器时真正需要知道的细节。
2. JSX 的底层逻辑与设计动因:为什么 React 不直接用 HTML 字符串?
2.1 JSX 不是 HTML:从编译产物反推设计意图
很多人以为 JSX 是“React 版 HTML”,这是最大的误解源头。我们来看一个最简单的例子:
const element = <h1>Hello, world!</h1>;
这段代码在浏览器里根本无法直接运行。它必须经过编译。Babel 的默认配置会把它转成:
const element = React.createElement(
"h1",
null,
"Hello, world!"
);
再复杂一点:
const element = (
<div className="container" data-id="123">
<p style={{ color: "blue", fontSize: "16px" }}>Text</p>
<button onClick={() => alert("clicked")}>Click me</button>
</div>
);
编译后是:
const element = React.createElement(
"div",
{
className: "container",
"data-id": "123"
},
React.createElement(
"p",
{ style: { color: "blue", fontSize: "16px" } },
"Text"
),
React.createElement(
"button",
{ onClick: () => alert("clicked") },
"Click me"
)
);
看到没?JSX 的嵌套结构,直接对应了
React.createElement
的参数嵌套。第一个参数是标签名(字符串或组件函数),第二个是 props 对象(所有属性都在这里),第三个及之后是子节点(可以是字符串、数字、其他
createElement
调用,甚至
null
或
undefined
)。这个结构决定了 JSX 的所有行为边界:
-
为什么
class变成className? 因为class是 JavaScript 的保留字,不能作为对象属性名。{ class: "xxx" }在 JS 中是语法错误,而{ className: "xxx" }完全合法。 -
为什么内联样式是对象
{ color: "blue" }而不是字符串"color: blue;"? 因为style属性在React.createElement的 props 对象里,必须是一个 JS 对象。字符串"color: blue;"是 CSS 文本,React 不会去解析它;而对象{ color: "blue" }是可被 JS 直接操作、合并、条件计算的数据结构。 -
为什么事件名是
onClick而不是onclick? 同样是 JS 命名规范。HTML 属性名不区分大小写,但 JS 对象属性名严格区分。{ onclick: fn }会被当作一个普通属性,而 React 的事件系统约定使用驼峰命名来标识这是一个 React 事件处理器,它会在内部做合成事件的封装和委托。
这个编译过程不是 React 的“黑魔法”,而是明确的设计选择:
让 UI 描述完全运行在 JavaScript 的语义体系内
。HTML 字符串是静态文本,无法直接参与 JS 的变量、条件、循环、函数调用;而 JSX 编译后的
createElement
调用,是纯 JS 表达式,可以被任意 JS 逻辑包裹。这才是 React “声明式 UI” 的根基。
2.2 与传统模板引擎的本质区别:JSX 是表达式,不是模板
对比一下常见的模板写法:
<!-- EJS 模板 -->
<% if (user) { %>
<h1>Welcome, <%= user.name %>!</h1>
<% } else { %>
<h1>Please login</h1>
<% } %>
<!-- Vue 模板 -->
<h1 v-if="user">Welcome, {{ user.name }}!</h1>
<h1 v-else>Please login</h1>
这些模板语法最终会被编译成字符串拼接或 DOM 操作指令。而 JSX 的条件渲染是这样写的:
function Greeting({ user }) {
return (
<div>
{user ? (
<h1>Welcome, {user.name}!</h1>
) : (
<h1>Please login</h1>
)}
</div>
);
}
这里的
{user ? ... : ...}
是一个 JavaScript 三元表达式,它被直接嵌入在 JSX 标签内部。JSX 解析器会识别花括号
{}
中的内容为 JS 表达式,并将其求值结果作为子节点插入。这意味着你可以写:
// 数组 map 渲染
{items.map((item, index) => (
<li key={index}>{item.text}</li>
))}
// 函数调用
{getHeaderTitle()}
// 甚至复杂的逻辑(不推荐,但语法上完全合法)
{isLoading ? <Spinner /> : data && <DataList data={data} />}
这种能力来源于 JSX 的解析规则:
花括号内必须是表达式(expression),不能是语句(statement)
。所以你不能在
{}
里写
if (x) {...}
或
for (let i=0; i<10; i++) {...}
,因为它们是语句。但你可以把逻辑提前写在函数里,然后在
{}
里调用函数。这种设计强制开发者将“逻辑”和“视图”在代码层面分离,而不是在模板语法层面混合。它牺牲了一点模板的“所见即所得”感,换来的是更强的可组合性、可测试性和类型安全(配合 TypeScript)。
2.3 为什么不用
document.createElement
?虚拟 DOM 的必要性
有人会问:既然最终都是创建元素,那为什么不直接用原生
document.createElement
?答案是性能和抽象层级。想象一个电商商品列表,有 100 个商品项,每个项包含图片、标题、价格、加入购物车按钮。如果每次价格变动都去
document.querySelector('.price').textContent = newPrice
,浏览器会触发重排(reflow)和重绘(repaint),性能极差。React 的解法是引入“虚拟 DOM”:
React.createElement
创建的不是真实 DOM 节点,而是一个轻量级的 JS 对象,描述“我想要什么样的 DOM”。当状态变化时,React 会生成一个新的虚拟 DOM 树,然后和旧树做 diff(差异对比),算出最小的 DOM 操作集合(比如只更新第 5 个商品的价格文本节点),最后批量应用到真实 DOM 上。这个过程对开发者是透明的,你只需要关心“数据变了,UI 应该长什么样”,而不必操心“怎么高效地改 DOM”。JSX 就是构建这个虚拟 DOM 树最自然的语法。它让你用接近 HTML 的方式,描述一个 JS 对象树,完美契合 React 的设计理念。
3. JSX 的核心语法详解与实操陷阱:从入门到避坑
3.1 基础语法:标签、属性、子节点的正确写法
JSX 的基础结构和 HTML 高度相似,但有几处关键差异,必须刻进肌肉记忆:
-
闭合规则 :所有标签必须闭合。
<img>、<input>这类自闭合标签,在 JSX 中必须写成<img />或<input />。写成<img>会被 Babel 当作有子节点的开始标签,导致编译错误。 -
属性命名 :HTML 属性名映射到 React props 时,遵循特定转换规则:
-
class→className(JS 保留字冲突) -
for→htmlFor(同上,for是 JS 循环关键字) -
tabindex→tabIndex(HTML 是小写,React 要求驼峰) -
aria-*属性保持原样:aria-label、aria-hidden等无需改动。 -
自定义 data 属性:
data-id、data-name等,直接写成data-id、data-name,React 会原样透传给 DOM。
-
-
布尔属性处理 :HTML 中
<input disabled>和<input disabled="disabled">等价。但在 JSX 中,disabled={true}和disabled={false}是明确的布尔值。<input disabled />等价于disabled={true};如果你想动态控制,必须显式绑定:<input disabled={isDisabled} />。如果isDisabled是false,该属性就不会出现在最终的 DOM 上。 -
子节点类型 :JSX 标签的子节点可以是多种类型:
-
字符串和数字:
<p>Hello {name}! You have {count} messages.</p>,其中name和count是变量。 -
null、undefined、true、false:这些值在渲染时会被忽略,不会产生任何输出。这常用于条件渲染:{showHeader && <Header />}。 -
数组:
{items.map(item => <li key={item.id}>{item.name}</li>)}。注意:数组中的每个元素 必须有唯一的key属性 ,这是 React 识别列表项变化的关键。key必须是字符串,且在同一列表中唯一。不要用index作为key(除非列表绝对静态、永不增删),因为当列表顺序变化时,index会错位,导致状态混乱。
-
字符串和数字:
提示:
key不是传给组件的 props,它是 React 内部使用的特殊属性。你在组件内部console.log(props)是看不到key的。它只在父组件的map循环中指定。
3.2 样式处理:内联 style 与 className 的实战选择
JSX 中设置样式只有两种官方方式:
className
(推荐)和
style
(内联)。没有第三种。
-
className是首选 :它对应 HTML 的class属性,用于挂载 CSS 类名。这是最符合 Web 标准、性能最好、也最利于维护的方式。// ✅ 推荐:使用 className,样式写在外部 CSS 文件里 <div className="card card--large"> <h2 className="card__title">Title</h2> <p className="card__content">Content</p> </div>这样写,CSS 可以复用、可缓存、支持媒体查询、伪类等所有 CSS 能力。现代前端工程中,通常会配合 CSS Modules 或 CSS-in-JS 方案(如 styled-components),实现样式作用域隔离。
-
style是补充 :它接收一个 JS 对象,键是 CSS 属性的驼峰形式(backgroundColor而非background-color),值是字符串或数字。// ✅ 合法:内联 style,用于动态、一次性样式 const dynamicStyle = { backgroundColor: isHovered ? '#007bff' : '#f8f9fa', transform: `scale(${scale})`, }; <div style={dynamicStyle}>Dynamic Content</div>style的典型场景是:动画过渡、根据数据计算的尺寸(如进度条宽度)、主题色切换等 必须由 JS 计算得出 的样式。它的缺点也很明显:无法继承、无法伪类、无法媒体查询、字符串拼接易出错、大量使用会拖慢渲染性能(因为每次 render 都要创建新对象)。
注意:
style对象的值如果是数字,React 会自动添加px单位(如fontSize: 16→font-size: 16px),但lineHeight、zIndex、opacity等无单位属性除外。所以lineHeight: 1.5是正确的,lineHeight: '1.5'也是正确的,但lineHeight: 1.5更简洁。
3.3 事件处理:从
onclick
到
onClick
的完整链条
JSX 中的事件处理是 React 最具特色的部分之一。它不是直接绑定到 DOM,而是通过 React 的“合成事件系统”(SyntheticEvent System)进行统一管理。
-
事件名必须驼峰 :
onClick、onChange、onSubmit、onMouseEnter。小写onclick会被忽略,且没有任何警告。 -
事件处理器必须是函数 :
<button onClick={handleClick}>是正确的;<button onClick={handleClick()}>是错误的,因为后者是立即执行函数并把返回值(通常是undefined)赋给onClick,导致点击时无响应。 -
合成事件对象 :事件处理器接收的参数是一个
SyntheticEvent对象,它模拟了原生浏览器事件,但提供了跨浏览器兼容性,并且是可池化的(提高性能)。你可以像操作原生事件一样访问e.target、e.preventDefault()等。function handleSubmit(e) { e.preventDefault(); // 阻止表单默认提交 console.log(e.target.elements.email.value); // 获取表单字段值 } <form onSubmit={handleSubmit}> <input name="email" /> <button type="submit">Submit</button> </form> -
事件委托与性能 :React 并不是给每个 JSX 元素都绑定原生事件监听器。它在文档根节点(
#root)上统一监听所有事件,然后根据事件冒泡路径,分发给对应的组件。这就是为什么你可以在父容器上监听子元素的点击事件(event delegation)。这也意味着,你不需要手动addEventListener/removeEventListener,React 会自动管理。
实操心得:我见过太多新手在
map循环里写onClick={() => handleClick(id)},这会导致每次 render 都创建一个新函数,破坏PureComponent或React.memo的浅比较优化。正确做法是:在循环外定义一个handleItemClick = (id) => { ... },然后在循环内写onClick={() => handleItemClick(id)},或者更优的,用useCallback包裹。
4. 从 JSX 到真实页面:完整工作流与环境搭建
4.1 开发环境初始化:Create React App 与 Vite 的选型对比
要让 JSX 代码跑起来,你需要一个能处理 JSX 语法的构建工具。目前主流选择是 Create React App(CRA)和 Vite。
-
Create React App(CRA) :Facebook 官方推出的零配置脚手架。它内置了 Webpack、Babel、ESLint,开箱即用。执行
npx create-react-app my-app,几秒后就能得到一个包含src/App.js(里面就是 JSX)的完整项目。CRA 的优势是稳定、生态成熟、文档丰富,特别适合初学者和企业级项目。但它启动和热更新稍慢,配置不够灵活(需eject才能自定义)。 -
Vite :新一代构建工具,主打极速冷启动和热更新。执行
npm create vite@latest my-app -- --template react,选择 React 模板,即可生成。Vite 使用原生 ES 模块(ESM)按需加载,无需打包,开发时速度极快。它对 JSX 的支持通过@vitejs/plugin-react插件实现,配置简单。Vite 更适合追求开发体验、喜欢折腾新工具的团队。
无论选哪个,核心流程一致:
-
项目初始化(
create-react-app或npm create vite) -
编写 JSX 组件(
.jsx或.tsx文件) -
在入口文件(
index.js)中,用ReactDOM.createRoot(...).render(<App />)将 JSX 元素挂载到 DOM -
运行
npm start,开发服务器启动,浏览器自动打开http://localhost:3000
注意:Vite 默认支持
.jsx文件,但如果你用 TypeScript,文件后缀应为.tsx,并在tsconfig.json中配置"jsx": "react-jsx",确保 TS 编译器能正确处理 JSX。
4.2 JSX 文件结构与组件拆分:从单文件到模块化
一个典型的 React 应用,JSX 代码不是散落在各处,而是组织在组件(Component)中。组件是可复用、可组合的 UI 单元。
-
函数组件(推荐) :现代 React 的标准写法,使用
function关键字或箭头函数定义,返回 JSX。// src/components/Button.jsx export default function Button({ children, onClick, variant = "primary" }) { return ( <button className={`btn btn--${variant}`} onClick={onClick} > {children} </button> ); } -
导入与使用 :在父组件中,通过
import导入并使用。// src/App.jsx import Button from './components/Button'; function App() { return ( <div> <h1>My App</h1> <Button onClick={() => alert('Hello')}>Click Me</Button> </div> ); } export default App;
这种模块化结构带来了巨大好处:
-
关注点分离
:
Button.jsx只关心按钮的外观和交互,App.jsx只关心整体布局和数据流。 -
可测试性
:你可以单独为
Button组件写单元测试,无需启动整个应用。 -
可维护性
:修改按钮样式,只需改
Button.jsx和对应的 CSS,不影响其他地方。
实操心得:我建议新手从“一个组件一个文件”开始,文件名和组件名保持一致(PascalCase)。避免把所有 JSX 堆在
App.jsx里,那是学习阶段的快捷方式,不是工程实践。
4.3 数据驱动 UI:Props、State 与 JSX 的协同工作
JSX 本身是静态的,但 React 的强大在于它能让 JSX “活”起来,响应数据变化。
-
Props(属性) :父组件向子组件传递数据的通道。它是只读的(immutable),子组件不能修改自己的 props。
// 父组件 <UserProfile name="Alice" age={30} /> // 子组件 UserProfile.jsx export default function UserProfile({ name, age }) { return <div><h2>{name}</h2><p>Age: {age}</p></div>; } -
State(状态) :组件内部可变的数据,通过
useStateHook 管理。状态变化会触发组件重新渲染,JSX 会基于新状态重新生成。import { useState } from 'react'; export default function Counter() { const [count, setCount] = useState(0); // 初始化 count 为 0 return ( <div> <p>Count: {count}</p> <button onClick={() => setCount(count + 1)}>+1</button> <button onClick={() => setCount(0)}>Reset</button> </div> ); }
这里的关键是:
{count}
是一个 JS 表达式,它在每次渲染时都会被求值。当
setCount
被调用,React 知道
count
变了,就会重新执行
Counter
函数,生成新的 JSX,然后 diff 更新 DOM。整个过程是声明式的:你只描述“UI 应该是什么样子”,React 负责“如何高效地变成那个样子”。
常见误区:新手常试图在 JSX 里直接修改 state,比如
onClick={() => count++}。这是无效的,因为count++只是改变了局部变量,React 完全不知道。必须通过setCount这样的 setter 函数来通知 React。
5. 面试高频考点与实战问题排查:JSX 相关的硬核问题
5.1 面试官最爱问的 5 个 JSX 问题及满分回答
React 面试中,JSX 是必考基础。以下是我整理的高频真题,附上技术要点和回答逻辑:
-
Q:JSX 到底是什么?它和 HTML 有什么区别?
A:JSX 是 JavaScript 的语法扩展,不是 HTML。它最终会被 Babel 编译成React.createElement()调用。区别在于:JSX 运行在 JS 引擎里,必须遵守 JS 语法规则(如className替代class);HTML 是纯文本标记,由浏览器解析。JSX 的优势是能无缝集成 JS 表达式(条件、循环、函数调用),实现真正的声明式 UI。 -
Q:为什么
class要写成className?
A:因为class是 JavaScript 的保留字,不能作为对象属性名。React.createElement的第二个参数是一个 props 对象,{ class: "xxx" }在 JS 中是语法错误,而{ className: "xxx" }是合法的。这是 JSX 编译规则决定的,不是 React 的随意规定。 -
Q:
key的作用是什么?为什么不能用index?
A:key是 React 识别列表项身份的唯一标识,用于 diff 算法判断哪些项被新增、删除或移动。用index作为key,当列表顺序变化(如排序、过滤)时,index会错位,导致 React 复用错误的 DOM 节点,引发状态混乱(如输入框内容错乱、动画异常)。正确做法是使用数据本身的唯一 ID,如item.id。 -
Q:
{this.state.items.map(...)}和{this.state.items.map(...)}有什么区别?(考察空格)
A:JSX 中,花括号{}内的表达式前后如果有空格,会被当作文本节点处理。{items.map(...)}是合法的,返回一个数组;{ items.map(...) }中的前导空格,会让 React 解析为{ " " + items.map(...) },即一个字符串加数组,这会报错Objects are not valid as a React child。所以{}内部不要随意加空格,尤其在表达式开头。 -
Q:如何在 JSX 中渲染 HTML 字符串?(如富文本)
A:不推荐直接渲染,有 XSS 风险。如果必须,使用dangerouslySetInnerHTML属性,它接受一个{ __html: string }对象。例如:<div dangerouslySetInnerHTML={{ __html: rawHtml }} />。但务必确保rawHtml是可信的、已过滤的,生产环境应使用DOMPurify等库进行净化。
5.2 真实开发中踩过的 3 个 JSX 坑及解决方案
这些不是教科书里的理论,而是我在多个项目中亲手踩出来的坑:
-
坑一:
map循环中忘记key,控制台报黄色警告,但页面看起来正常
现象:控制台出现Warning: Each child in a list should have a unique "key" prop.,但 UI 没问题。很多新手就忽略了。
后果:当列表动态变化(如搜索过滤、实时更新)时,React 无法正确追踪节点,导致状态错乱。比如一个待办列表,勾选了第 2 项,过滤后第 2 项消失,再取消过滤,原来第 2 项的勾选状态可能跑到第 1 项上。
解决方案: 永远为map的每个子元素显式提供key。如果数据没有 ID,宁可生成一个临时 ID(如uuid()),也不要妥协用index。 -
坑二:
style对象在每次 render 时都创建新对象,导致子组件不必要的重渲染
现象:一个PureComponent或用React.memo包裹的子组件,明明 props 没变,却频繁 re-render。
原因:父组件的 JSX 中写了<Child style={{ color: 'red' }} />,每次父组件 render,{ color: 'red' }都是一个新对象,===比较为false,触发子组件更新。
解决方案:将style对象提取到组件外部(如果静态),或用useMemo缓存(如果依赖 props)。const staticStyle = { color: 'red' }; // ✅ 外部定义,引用不变 <Child style={staticStyle} /> // 或 const dynamicStyle = useMemo(() => ({ color: isRed ? 'red' : 'blue' }), [isRed]); // ✅ 缓存 <Child style={dynamicStyle} /> -
坑三:在 JSX 中直接调用异步函数,导致
Promise对象被当作 React child 渲染
现象:<div>{fetchData()}</div>,控制台报错Objects are not valid as a React child。
原因:fetchData()返回一个Promise,而 React 只能渲染字符串、数字、JSX 元素、数组、null、undefined、boolean,不能渲染Promise。
解决方案: JSX 中只能放同步表达式 。异步逻辑必须在useEffect或事件处理器中执行,数据状态用useState管理,然后在 JSX 中渲染state。function DataComponent() { const [data, setData] = useState(null); useEffect(() => { fetchData().then(setData); // ✅ 在 effect 中处理异步 }, []); return <div>{data ? <p>{data.title}</p> : <Spinner />}</div>; // ✅ 渲染 state }
5.3 JSX 错误排查速查表
| 错误信息 | 可能原因 | 快速定位方法 | 解决方案 |
|---|---|---|---|
Parsing error: Adjacent JSX elements must be wrapped in an enclosing tag
| JSX 根节点有多个兄弟元素,未用父容器包裹 |
检查
return
语句内的 JSX,看是否有两个并列的
<div>
|
用
<Fragment>
(
<>...</>
) 或一个
<div>
包裹所有兄弟节点
|
TypeError: Cannot read property 'map' of undefined
|
尝试对
undefined
值调用
map
|
在
map
前加
items && items.map(...)
或
items?.map(...)
(可选链)
| 确保数据加载完成后再渲染列表,或提供默认空数组 `items |
Warning: validateDOMNesting: <div> cannot appear as a child of <p>
|
HTML 嵌套规则被违反(如
<p>
里不能放
<div>
)
| 查看浏览器 Elements 面板,找到报错的 DOM 节点 |
改用语义化正确的标签,如
<p>
里只放行内元素,块级元素用
<div>
或
<section>
|
React Hook "useState" is called conditionally
|
useState
等 Hook 在条件语句或循环中调用
|
检查
useState
是否在
if
、
for
、
map
回调等非顶层位置
| 所有 Hooks 必须在组件顶层调用,不能在条件、循环、嵌套函数中 |
最后一个小技巧:VS Code 安装
ES7+ React/Redux/React-Native snippets插件,输入rfc回车,就能快速生成一个标准的函数组件骨架,省去重复劳动。这是每个 React 开发者都应该掌握的效率神器。

386

被折叠的 条评论
为什么被折叠?



