03、React JSX

你是否曾经在JavaScript里用字符串拼接HTML,拼到怀疑人生?是否曾经因为一个引号的遗漏,导致整个页面白屏排查到深夜?是否曾经在维护一段充斥着innerHTML的代码时,恨不得重写整个项目?
如果你有过以上任何一种经历,那么这篇文章就是为你而写。今天我们来彻底搞懂React生态中最核心、最具争议、同时也是最强大的语法特性------JSX。
本文将从真实开发痛点出发,带你穿越JSX的演进历史,拆解其底层原理,掌握企业级项目中的实战用法,并最终从容应对面试官的连环追问。无论你是刚入门的前端小白,还是有一定经验的开发者,读完这篇文章,你对JSX的理解都将上升一个维度。
文章目录
- [03、React JSX](#03、React JSX)
-
- 一、痛点场景描述
-
- [1.1 字符串拼接的噩梦](#1.1 字符串拼接的噩梦)
- [1.2 XSS安全隐患](#1.2 XSS安全隐患)
- [1.3 逻辑与视图的割裂](#1.3 逻辑与视图的割裂)
- [1.4 企业级项目的真实困境](#1.4 企业级项目的真实困境)
- 二、痛点的解决方案
-
- [2.1 用JSX重写用户列表](#2.1 用JSX重写用户列表)
- [2.2 自动转义,安全无忧](#2.2 自动转义,安全无忧)
- [2.3 逻辑与视图的统一](#2.3 逻辑与视图的统一)
- 三、JSX是什么
-
- [3.1 专业解释](#3.1 专业解释)
- [3.2 大白话解释](#3.2 大白话解释)
- [3.3 生活案例](#3.3 生活案例)
- 四、为什么要用JSX
-
- [4.1 可读性与开发效率](#4.1 可读性与开发效率)
- [4.2 编译期错误检测](#4.2 编译期错误检测)
- [4.3 类型安全](#4.3 类型安全)
- [4.4 性能优化](#4.4 性能优化)
- [4.5 生态与工具链支持](#4.5 生态与工具链支持)
- 五、JSX是怎么演进过来的
-
- [5.1 思想源头:Facebook的XHP(2010年)](#5.1 思想源头:Facebook的XHP(2010年))
- [5.2 React诞生与JSX引入(2013年)](#5.2 React诞生与JSX引入(2013年))
- [5.3 Babel成为标准编译器(2015年)](#5.3 Babel成为标准编译器(2015年))
- [5.4 TypeScript支持TSX(2016年)](#5.4 TypeScript支持TSX(2016年))
- [5.5 React 17新JSX转换(2020年)](#5.5 React 17新JSX转换(2020年))
- [5.6 React 19与React Compiler(2024年至今)](#5.6 React 19与React Compiler(2024年至今))
- 六、JSX怎么用
-
- [6.1 基本规则](#6.1 基本规则)
- [6.2 JavaScript表达式嵌入](#6.2 JavaScript表达式嵌入)
- [6.3 条件渲染](#6.3 条件渲染)
- [6.4 列表渲染](#6.4 列表渲染)
- [6.5 样式处理](#6.5 样式处理)
- [6.6 事件处理](#6.6 事件处理)
- [6.7 注释](#6.7 注释)
- 七、企业项目中如何使用JSX
-
- [7.1 条件渲染的高级模式](#7.1 条件渲染的高级模式)
- [7.2 动态组件渲染](#7.2 动态组件渲染)
- [7.3 列表渲染的性能优化](#7.3 列表渲染的性能优化)
- [7.4 组合模式:children与插槽](#7.4 组合模式:children与插槽)
- [7.5 渲染属性(Render Props)模式](#7.5 渲染属性(Render Props)模式)
- 八、常用场景教学
-
- [8.1 场景一:表单处理](#8.1 场景一:表单处理)
- [8.2 场景二:搜索与筛选](#8.2 场景二:搜索与筛选)
- [8.3 场景三:标签页(Tabs)切换](#8.3 场景三:标签页(Tabs)切换)
- [8.4 场景四:模态框(Modal)](#8.4 场景四:模态框(Modal))
- 九、竞品对比:前端模板方案优劣势分析
-
- [9.1 四种方案概览](#9.1 四种方案概览)
- [9.2 多维度对比表](#9.2 多维度对比表)
- [9.3 各方案优劣势详解](#9.3 各方案优劣势详解)
- [9.4 选型建议](#9.4 选型建议)
- 十、面试官高频面试题
-
- 面试题一:什么是JSX?它的本质是什么?
- 面试题二:JSX中的花括号{}里面可以写什么?不能写什么?
- 面试题三:为什么JSX中用className而不是class?用htmlFor而不是for?
- 面试题四:列表渲染中为什么需要key?key的作用是什么?
- [面试题五:React 17的新JSX转换和旧转换有什么区别?](#面试题五:React 17的新JSX转换和旧转换有什么区别?)
- 面试题六:JSX和HTML有什么区别?
- 面试题七:JSX中如何实现条件渲染?有哪些方式?
- 面试题八:JSX是如何被编译成JavaScript的?简述Babel的编译流程。
- 面试题九:JSX中的Fragment是什么?为什么需要它?
- 面试题十:JSX中的事件处理和原生HTML事件有什么区别?
- 结语
一、痛点场景描述

在深入JSX之前,我们先回到那个没有JSX的"黑暗时代",看看前端开发者究竟面临着怎样的困境。
1.1 字符串拼接的噩梦
在早期的前端开发中,动态生成HTML最常见的方式就是字符串拼接。想象一下,你需要根据后端返回的数据渲染一个用户列表:
javascript
// 传统方式:字符串拼接HTML
function renderUserList(users) {
var html = '<ul class="user-list">';
for (var i = 0; i < users.length; i++) {
html += '<li class="user-item">' +
'<img src="' + users[i].avatar + '" alt="' + users[i].name + '">' +
'<div class="user-info">' +
'<h3>' + users[i].name + '</h3>' +
'<p>' + users[i].bio + '</p>' +
'</div>' +
'</li>';
}
html += '</ul>';
document.getElementById('app').innerHTML = html;
}
这段代码看起来还算规整,但实际项目中远比这复杂。当嵌套层级加深、条件判断增多、数据字段变化时,字符串拼接就会变成一场灾难:
- 引号嵌套问题:单引号里套双引号,双引号里又要转义单引号,稍不留神就语法错误
- 代码可读性极差:HTML结构被拆得支离破碎,无法一眼看出DOM层级
- 维护成本高昂:修改一个标签的class名,可能要在几十个拼接片段中寻找
- 调试困难:拼错了一个标签,浏览器只会给你一个模糊的渲染错误
1.2 XSS安全隐患
使用innerHTML插入HTML内容时,如果数据中包含恶意脚本,就会直接执行:
javascript
// 安全隐患:用户输入直接插入HTML
var userInput = '<img src=x onerror="alert(\'XSS攻击\')">';
document.getElementById('app').innerHTML = userInput; // 恶意代码被执行
在企业项目中,用户输入、评论内容、富文本编辑器等场景无处不在,每一处innerHTML都可能成为安全漏洞。开发者不得不手动编写转义函数,在每一个数据插入点小心翼翼地处理。
1.3 逻辑与视图的割裂
传统的开发模式强调"关注点分离"------HTML写结构、CSS写样式、JS写逻辑,三者分离。但在实际的复杂交互页面中,这种分离反而造成了割裂:
- 模板文件里写了一堆占位符,JS文件里再去一个个查找替换
- 一个按钮的点击事件,定义在JS里,绑定在HTML里,处理逻辑又在另一个函数里
- 修改一个功能,需要在三个文件之间来回跳转
正如React官方文档所言:"将渲染逻辑与标记语言放在一起,使得创建、维护和删除组件变得容易。"传统的分离模式,在组件化开发的浪潮下,已经显得力不从心。
1.4 企业级项目的真实困境
在中大型企业项目中,这些痛点被无限放大:
- 一个页面可能包含上百个动态元素,字符串拼接代码动辄数千行
- 多人协作时,A改了模板结构,B的JS选择器全部失效
- 新功能迭代时,没人敢动老的渲染代码,只能不断叠加新的拼接逻辑
- 代码审查时,没有人愿意读那一堆引号和加号组成的"天书"
这就是JSX诞生之前,前端开发者面临的真实困境。而JSX的出现,正是为了系统性地解决这些问题。
二、痛点的解决方案

面对上述痛点,React团队给出的答案就是JSX。它不是一个全新的语言,不是一个模板引擎,而是JavaScript的语法扩展------让你能够在JS代码中直接书写类HTML的标记。
2.1 用JSX重写用户列表
我们用JSX重写上面那个用户列表的例子,感受一下差异:
jsx
// JSX方式:直观的类HTML语法
function renderUserList(users) {
return (
<ul className="user-list">
{users.map(user => (
<li key={user.id} className="user-item">
<img src={user.avatar} alt={user.name} />
<div className="user-info">
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
</li>
))}
</ul>
);
}
同样的功能,JSX版本的优势一目了然:
- HTML结构完整呈现,DOM层级清晰可见
- 数据绑定用
{}包裹,不再需要引号拼接 - 原生JavaScript的
map方法直接用于列表渲染 - 代码缩进即结构,可读性大幅提升
2.2 自动转义,安全无忧
JSX默认会对插入的内容进行HTML转义,从根本上杜绝XSS攻击:
jsx
// JSX自动转义,防止XSS
const userInput = '<img src=x onerror="alert(\'XSS攻击\')">';
const element = <div>{userInput}</div>;
// 渲染结果:文本"<img src=x onerror=...>"被原样显示为文本,不会执行
如果你确实需要插入原始HTML,React也提供了明确的APIdangerouslySetInnerHTML,从命名上就提醒你这是危险操作,需要谨慎使用。
2.3 逻辑与视图的统一
JSX打破了传统的"模板与逻辑分离"的教条,转而倡导"关注点分离"的真正含义------按组件分离。一个组件的结构、逻辑、样式内聚在一起,而不是按技术栈分散在不同文件中:
jsx
// 组件化:结构与逻辑内聚
function UserCard({ user }) {
const [isFollowing, setIsFollowing] = useState(false);
const handleFollow = () => {
setIsFollowing(!isFollowing);
};
return (
<div className="user-card">
<img src={user.avatar} alt={user.name} />
<h3>{user.name}</h3>
<p>{user.bio}</p>
<button onClick={handleFollow}>
{isFollowing ? '已关注' : '关注'}
</button>
</div>
);
}
在这个组件中,状态管理、事件处理、UI结构全部集中在一处,修改功能时不需要在多个文件间跳转。
三、JSX是什么

3.1 专业解释
JSX(JavaScript XML)是一种JavaScript的语法扩展,它允许你在JavaScript代码中书写类似XML/HTML的标记语法。JSX最终会被Babel或TypeScript等编译器转换为标准的React.createElement()函数调用,生成React元素(即虚拟DOM对象)。
用React官方术语表的定义来说:"JSX是一个JavaScript语法扩展。它类似于模板语言,但它具有JavaScript的全部能力。JSX最终会被编译为React.createElement()函数调用,返回称为'React元素'的普通JavaScript对象。"
核心要点:
- JSX不是模板引擎,它没有自己的一套语法规则
- JSX不是字符串,它不会在运行时被解析
- JSX是编译期的语法糖,编译后就是纯JavaScript函数调用
- JSX具有JavaScript的全部能力,你可以在其中使用任何JS表达式
3.2 大白话解释
说人话,JSX就是让你在JavaScript里写HTML的一种"作弊码"。
你可以把它想象成一个翻译官:你在代码里写的是看起来像HTML的东西,但浏览器不认识这种语法。于是Babel这个翻译官在代码运行之前,把这些"HTML"一句句翻译成浏览器认识的JavaScript函数调用。
你写的是:
jsx
const element = <h1 className="title">Hello JSX</h1>;
Babel翻译后变成:
javascript
const element = React.createElement('h1', { className: 'title' }, 'Hello JSX');
就这么简单。JSX本质上就是React.createElement的简写形式,让你不用写一堆嵌套的函数调用,而是用更直观的标签语法来描述UI结构。
3.3 生活案例
为了让你彻底理解JSX的本质,我们来看一个生活中的类比。
想象你在一家餐厅点餐。有两种点餐方式:
方式一:逐项描述(类似React.createElement)
你对服务员说:"我要一个盘子,盘子里放一块牛排,牛排要七分熟,旁边配一份蔬菜沙拉,沙拉用千岛酱,再来一杯可乐,加冰。"
服务员需要把你的每一句话记录下来,然后转告厨房。这种方式虽然精确,但说起来很累,听的人也容易遗漏。
方式二:看菜单图片点餐(类似JSX)
你直接指着菜单上的图片说:"我要这个套餐。"菜单上的图片直观地展示了最终成品的样子,你一眼就能看出自己点的是什么,服务员也不会理解错。
JSX就是那个"菜单图片"。它让你用直观的标签语法描述UI最终的样子,而不需要用一层层的函数调用去"逐项描述"。编译器(服务员)会自动把这个直观的描述转换成底层的函数调用(厨房指令)。
再举一个更贴近程序员的例子。你写SQL的时候,写的是SELECT * FROM users WHERE age > 18,而不是直接操作数据库的底层存储引擎。SQL是一种声明式的语法,你描述"要什么",而不是"怎么做"。JSX同理,你描述"UI长什么样",React负责"怎么渲染到页面上"。
四、为什么要用JSX

理解了JSX是什么之后,一个自然的问题是:为什么一定要用JSX?直接用React.createElement不行吗?
答案是:技术上完全可以,但你不会想那么做。JSX带来的价值是全方位的。
4.1 可读性与开发效率
这是JSX最直观的优势。对比下面两段等价代码:
jsx
// JSX版本
const layout = (
<div className="container">
<header className="header">
<h1>{title}</h1>
<nav>
<a href="/home">首页</a>
<a href="/about">关于</a>
</nav>
</header>
<main className="content">{children}</main>
<footer className="footer">
<p>© 2024 My App</p>
</footer>
</div>
);
javascript
// 纯React.createElement版本
const layout = React.createElement('div', { className: 'container' },
React.createElement('header', { className: 'header' },
React.createElement('h1', null, title),
React.createElement('nav', null,
React.createElement('a', { href: '/home' }, '首页'),
React.createElement('a', { href: '/about' }, '关于')
)
),
React.createElement('main', { className: 'content' }, children),
React.createElement('footer', { className: 'footer' },
React.createElement('p', null, '© 2024 My App')
)
);
两段代码功能完全相同,但JSX版本的可读性碾压级胜出。在实际项目中,组件的复杂度远高于此,没有JSX的代码将完全不可维护。
4.2 编译期错误检测
JSX在编译阶段就能发现很多错误,而不是等到运行时才暴露:
- 标签未闭合:编译时报错
- 属性名拼写错误:TypeScript或ESLint可以检测
- 引用了不存在的组件:编译时报错
- 花括号内表达式语法错误:编译时报错
这种"左移"的错误检测能力,在企业级项目中至关重要。一个在编译期发现的错误,修复成本可能只有运行时错误的十分之一。
4.3 类型安全
配合TypeScript使用时,JSX(此时称为TSX)提供了完整的类型检查:
tsx
// TSX中的类型检查
interface UserProps {
name: string;
age: number;
avatar: string;
}
function UserCard({ name, age, avatar }: UserProps) {
return (
<div>
<img src={avatar} alt={name} />
<h2>{name}</h2>
<p>年龄:{age}</p>
</div>
);
}
// 编译错误:缺少必需属性age
<UserCard name="张三" avatar="/avatar.png" />
// 编译错误:age类型不匹配
<UserCard name="张三" age="二十五" avatar="/avatar.png" />
类型系统能够在你写代码的时候就提示属性缺失、类型不匹配等问题,这是传统模板字符串方案无法比拟的。
4.4 性能优化
JSX在编译阶段就完成了从标签到函数调用的转换,运行时不需要额外的模板解析开销。同时,React的编译器可以基于JSX的静态结构进行优化:
- React 17的新JSX转换允许编译器生成更精简的代码
- React Compiler可以自动优化不必要的重渲染
- 静态提升(Static Hoisting)将不变的JSX元素提升到组件外部
4.5 生态与工具链支持
JSX已经成为前端生态的事实标准:
- VSCode、WebStorm等IDE对JSX有完善的语法高亮、自动补全、重构支持
- ESLint、Prettier等工具链原生支持JSX
- React Native、SolidJS、Vue 3等框架都支持JSX语法
- 大量第三方组件库基于JSX构建
选择JSX,就是选择了整个生态的支持。
五、JSX是怎么演进过来的

任何技术的诞生都不是凭空出现的,JSX也有其清晰的演进脉络。了解这段历史,能帮助你更深刻地理解JSX的设计哲学。
5.1 思想源头:Facebook的XHP(2010年)
JSX的思想源头可以追溯到Facebook在2010年推出的XHP。XHP是PHP的一个扩展,允许开发者在PHP代码中直接书写XML语法:
php
// XHP示例:在PHP中写XML
class :blog:post extends :x:element {
protected function render() {
return (
<div class="post">
<h1>{$this->getTitle()}</h1>
<div class="content">{$this->getContent()}</div>
</div>
);
}
}
XHP最初的设计目标是防止XSS攻击------因为内容通过XML语法插入时会自动转义。但Facebook的工程师们很快发现,这种"在代码中写标记"的方式,在构建可复用组件方面有着巨大的优势。
5.2 React诞生与JSX引入(2013年)
2013年5月,Facebook在JSConf US上开源了React,同时带来了JSX语法。当时React的核心开发者Jordan Walke受到XHP的启发,将"在JavaScript中写XML"的想法带入了前端世界。
有趣的是,JSX在刚推出时引发了巨大的争议。许多开发者认为"在JS里混HTML"是对"关注点分离"原则的违背,甚至有人称之为"倒退"。争议之大,以至于React团队不得不提供React.createElement这个无JSX的API作为替代方案。
但历史证明了JSX的价值。在短短一年内,JSX就成为了React开发的标准方式,当初的反对者们纷纷"真香"。
5.3 Babel成为标准编译器(2015年)
2015年,Babel(前身是6to5)成为了JSX到JavaScript的标准转译工具。在此之前,React有自己的JSX转换工具react-tools,但Babel的出现统一了生态------它不仅能转译JSX,还能转译ES6+、TypeScript等各种JavaScript超集。
同年,React Native发布,将JSX的应用范围从Web扩展到了移动端。开发者可以用同样的JSX语法编写iOS和Android应用,"一次学习,随处编写"的理念开始深入人心。
5.4 TypeScript支持TSX(2016年)
2016年,TypeScript 1.8版本正式加入了对JSX的支持,文件扩展名为.tsx。这意味着JSX获得了完整的类型检查能力,企业级项目开始大规模采用TypeScript + React的技术栈。
TSX的出现,是JSX从"好用的语法糖"迈向"企业级开发基石"的关键一步。
5.5 React 17新JSX转换(2020年)
2020年10月,React 17发布,其中一个重要变化就是新的JSX转换。在React 17之前,JSX会被编译为React.createElement调用,这意味着你必须在每个文件中import React:
jsx
// React 16及之前:必须import React
import React from 'react';
function App() {
return <h1>Hello</h1>;
}
React 17的新转换将JSX编译为从react/jsx-runtime导入的jsx函数:
jsx
// React 17及之后:不需要import React
function App() {
return <h1>Hello</h1>;
}
编译后的代码大致如下:
javascript
import { jsx as _jsx } from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello' });
}
这个变化带来了三个好处:
- 不再需要手动
import React,减少了样板代码 - 编译输出更精简,打包体积略有减小
- 为未来的React编译器优化奠定了基础
5.6 React 19与React Compiler(2024年至今)
2024年12月React 19发布,带来了Actions、use hook、稳定的Server Components等特性。同时,React Compiler 1.0正式推出,它能够自动优化组件的重渲染,而开发者无需手动使用useMemo、useCallback。
React Compiler的工作基础正是JSX的静态可分析性------因为JSX的结构在编译期就是确定的,编译器可以精确地分析哪些值变化了、哪些组件需要重渲染,从而自动生成优化后的代码。
从XHP到React Compiler,JSX走过了十几年的演进之路。它从一个备受争议的实验性语法,成长为整个前端生态的基石之一。
六、JSX怎么用

理论讲了这么多,接下来进入实战环节。我们从零开始,系统掌握JSX的核心语法。
6.1 基本规则
JSX有几条必须遵守的基本规则,这些也是面试中经常考察的点。
规则一:必须有一个根元素
JSX表达式必须返回单个根元素。如果你需要返回多个同级元素,可以用<div>包裹,或者使用React Fragment(<></>):
jsx
// 错误:返回了两个同级元素
function BadExample() {
return (
<h1>标题</h1>
<p>内容</p>
);
}
// 正确:用div包裹
function GoodExample1() {
return (
<div>
<h1>标题</h1>
<p>内容</p>
</div>
);
}
// 正确:使用Fragment,不产生额外DOM节点
function GoodExample2() {
return (
<>
<h1>标题</h1>
<p>内容</p>
</>
);
}
规则二:标签必须闭合
JSX要求所有标签都必须正确闭合。自闭合标签(如<img>、<input>、<br>)必须写成<img />的形式:
jsx
// 错误
<img src="logo.png" alt="logo">
// 正确
<img src="logo.png" alt="logo" />
规则三:使用className而非class
由于class是JavaScript的保留字,JSX中使用className来设置HTML的class属性:
jsx
// 错误
<div class="container">内容</div>
// 正确
<div className="container">内容</div>
同理,for属性要写成htmlFor:
jsx
<label htmlFor="username">用户名</label>
<input id="username" type="text" />
规则四:属性名使用驼峰命名
JSX中的DOM属性使用驼峰命名法,而非HTML中的短横线命名:
jsx
// HTML中的写法
<div onclick="handleClick()" tabindex="0" data-id="123"></div>
// JSX中的写法
<div onClick={handleClick} tabIndex={0} data-id="123"></div>
注意:data-*和aria-*属性保持原有的短横线写法不变。
6.2 JavaScript表达式嵌入
JSX中最核心的语法就是花括号{}。在花括号中,你可以写任意合法的JavaScript表达式:
jsx
function ExpressionDemo() {
const name = '羽师';
const isLoggedIn = true;
const user = { age: 28, city: '广州' };
return (
<div>
{/* 变量 */}
<h1>你好,{name}</h1>
{/* 运算表达式 */}
<p>明年你将{user.age + 1}岁</p>
{/* 函数调用 */}
<p>城市:{user.city.toUpperCase()}</p>
{/* 三元表达式 */}
<p>{isLoggedIn ? '已登录' : '未登录'}</p>
{/* 方法调用 */}
<p>{[1, 2, 3].join(' - ')}</p>
</div>
);
}
注意,花括号中只能写表达式 ,不能写语句。以下写法是错误的:
jsx
// 错误:if语句不能直接写在花括号中
<div>{if (isLoggedIn) { '已登录' } else { '未登录' }}</div>
// 错误:for循环不能直接写在花括号中
<ul>{for (let i = 0; i < 5; i++) { <li>{i}</li> }}</ul>
对于条件判断,使用三元表达式或逻辑与&&;对于循环,使用数组的map方法。
6.3 条件渲染
JSX中实现条件渲染有三种常用方式:
方式一:if语句(组件外部)
jsx
function Greeting({ isLoggedIn }) {
if (isLoggedIn) {
return <h1>欢迎回来!</h1>;
}
return <h1>请先登录</h1>;
}
方式二:三元表达式(JSX内部)
jsx
function Greeting({ isLoggedIn }) {
return (
<div>
<h1>{isLoggedIn ? '欢迎回来!' : '请先登录'}</h1>
</div>
);
}
方式三:逻辑与&&(满足条件才渲染)
jsx
function Notification({ count }) {
return (
<div>
<h1>消息中心</h1>
{count > 0 && <span className="badge">你有{count}条未读消息</span>}
</div>
);
}
这里有一个常见的坑:当count为0时,count > 0 && ...会正确地不渲染后面的内容,但如果你写的是count && ...,那么0会被渲染到页面上。正确的做法是确保&&左侧是布尔值:
jsx
// 潜在问题:count为0时会渲染"0"
{count && <span>{count}条消息</span>}
// 正确写法:显式转换为布尔值
{count > 0 && <span>{count}条消息</span>}
{Boolean(count) && <span>{count}条消息</span>}
6.4 列表渲染
列表渲染使用数组的map方法,每个元素必须指定唯一的key属性:
jsx
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => (
<li key={todo.id}>
<input type="checkbox" checked={todo.completed} />
<span>{todo.text}</span>
</li>
))}
</ul>
);
}
关于key的使用,有几个重要原则:
- key必须是唯一的,在兄弟节点之间不能重复
- key应该是稳定的,不要使用数组索引作为key(除非列表是静态的、不会排序或增删)
- key不需要全局唯一,只需要在兄弟节点间唯一
jsx
// 不推荐:使用索引作为key(列表动态变化时会出问题)
{todos.map((todo, index) => (
<li key={index}>{todo.text}</li>
))}
// 推荐:使用数据的唯一ID作为key
{todos.map(todo => (
<li key={todo.id}>{todo.text}</li>
))}
6.5 样式处理
JSX中有两种设置样式的方式:
方式一:内联样式(style属性接收对象)
jsx
function InlineStyleDemo() {
const headingStyle = {
color: '#FF00FF',
fontSize: '24px',
backgroundColor: '#f0f0f0',
padding: '16px',
borderRadius: '8px'
};
return (
<div>
<h1 style={headingStyle}>带内联样式的标题</h1>
<p style={{ color: 'green', fontSize: 16 }}>
注意:数字值会自动添加px单位
</p>
</div>
);
}
内联样式的属性名使用驼峰命名(fontSize而非font-size),数字值会自动添加px单位(但lineHeight、flex等无单位属性除外)。
方式二:className配合CSS文件
jsx
import './Button.css';
function Button({ children, variant = 'primary' }) {
return (
<button className={`btn btn-${variant}`}>
{children}
</button>
);
}
6.6 事件处理
JSX中的事件处理使用驼峰命名的属性,值为函数引用:
jsx
function EventDemo() {
const [count, setCount] = useState(0);
const handleClick = () => {
setCount(count + 1);
};
const handleInputChange = (e) => {
console.log('输入值:', e.target.value);
};
return (
<div>
<button onClick={handleClick}>点击了{count}次</button>
<input type="text" onChange={handleInputChange} />
</div>
);
}
传递参数时,使用箭头函数包裹:
jsx
function UserList({ users, onDelete }) {
return (
<ul>
{users.map(user => (
<li key={user.id}>
{user.name}
<button onClick={() => onDelete(user.id)}>删除</button>
</li>
))}
</ul>
);
}
6.7 注释
JSX中的注释需要写在花括号内:
jsx
function CommentDemo() {
return (
<div>
{/* 这是JSX中的注释 */}
<h1>标题</h1>
{
// 也可以写单行注释,但需要换行
}
<p>内容</p>
</div>
);
}
七、企业项目中如何使用JSX

掌握了基础语法之后,我们来看看在真实的企业级项目中,JSX是如何被运用的。以下内容均来自实际项目中的最佳实践。
7.1 条件渲染的高级模式
在企业项目中,条件渲染往往比简单的if/else复杂得多。以下是几种常用的高级模式。
多分支条件渲染:使用对象映射
jsx
// 根据状态渲染不同内容,避免冗长的if-else链
const statusConfig = {
pending: { text: '待处理', color: '#faad14' },
processing: { text: '处理中', color: '#1890ff' },
success: { text: '已完成', color: '#52c41a' },
failed: { text: '失败', color: '#ff4d4f' }
};
function StatusBadge({ status }) {
const config = statusConfig[status] || statusConfig.pending;
return (
<span style={{ color: config.color }}>
{config.text}
</span>
);
}
权限控制:高阶组件封装
jsx
// 权限控制高阶组件
function withPermission(WrappedComponent, requiredRole) {
return function PermissionWrapper(props) {
const { userRole } = useAuth();
if (userRole !== requiredRole) {
return <div className="no-permission">您没有权限访问此内容</div>;
}
return <WrappedComponent {...props} />;
};
}
// 使用
const AdminPanel = withPermission(AdminDashboard, 'admin');
加载状态与错误状态的统一处理
jsx
// 数据加载状态统一封装
function withDataFetching(WrappedComponent, fetchData) {
return function DataWrapper(props) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
setLoading(true);
fetchData(props)
.then(result => {
setData(result);
setLoading(false);
})
.catch(err => {
setError(err);
setLoading(false);
});
}, [props.id]);
if (loading) {
return <div className="loading">加载中...</div>;
}
if (error) {
return <div className="error">加载失败:{error.message}</div>;
}
return <WrappedComponent data={data} {...props} />;
};
}
7.2 动态组件渲染
在企业项目中,经常需要根据配置动态渲染不同的组件。JSX的标签名可以是变量,这为动态组件提供了可能:
jsx
// 根据配置动态渲染组件
const componentMap = {
text: TextField,
number: NumberField,
select: SelectField,
date: DateField,
checkbox: CheckboxField
};
function DynamicForm({ fields }) {
return (
<form>
{fields.map(field => {
const Component = componentMap[field.type];
if (!Component) {
console.warn(`未知的字段类型:${field.type}`);
return null;
}
return <Component key={field.name} {...field} />;
})}
</form>
);
}
// 使用示例
const formFields = [
{ type: 'text', name: 'username', label: '用户名' },
{ type: 'number', name: 'age', label: '年龄' },
{ type: 'select', name: 'city', label: '城市', options: [...] },
{ type: 'date', name: 'birthday', label: '生日' }
];
<DynamicForm fields={formFields} />
注意:动态组件的变量名必须大写开头,否则React会将其视为HTML标签。
7.3 列表渲染的性能优化
在渲染大型列表时,性能优化至关重要。以下是企业项目中的常用优化手段。
使用React.memo避免不必要的重渲染
jsx
// 列表项组件用React.memo包裹
const TodoItem = React.memo(function TodoItem({ todo, onToggle, onDelete }) {
return (
<li className="todo-item">
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
<span className={todo.completed ? 'completed' : ''}>
{todo.text}
</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
);
});
function TodoList({ todos, onToggle, onDelete }) {
return (
<ul>
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggle}
onDelete={onDelete}
/>
))}
</ul>
);
}
虚拟滚动:处理超大数据量
jsx
// 使用react-window实现虚拟滚动(仅渲染可见区域的元素)
import { FixedSizeList as List } from 'react-window';
function VirtualList({ items }) {
const Row = ({ index, style }) => (
<div style={style} className="list-row">
{items[index].name}
</div>
);
return (
<List
height={500}
itemCount={items.length}
itemSize={50}
width="100%"
>
{Row}
</List>
);
}
7.4 组合模式:children与插槽
JSX的children属性提供了强大的组件组合能力,类似于Vue中的插槽:
jsx
// 卡片组件:通过children自定义内容
function Card({ title, children, footer }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
{footer && <div className="card-footer">{footer}</div>}
</div>
);
}
// 使用
<Card
title="用户信息"
footer={<button onClick={handleSave}>保存</button>}
>
<div className="form-group">
<label>用户名</label>
<input type="text" value={name} onChange={e => setName(e.target.value)} />
</div>
<div className="form-group">
<label>邮箱</label>
<input type="email" value={email} onChange={e => setEmail(e.target.value)} />
</div>
</Card>
7.5 渲染属性(Render Props)模式
渲染属性是一种通过函数作为children来共享逻辑的高级模式:
jsx
// 鼠标位置追踪组件
function MouseTracker({ children }) {
const [position, setPosition] = useState({ x: 0, y: 0 });
const handleMouseMove = (e) => {
setPosition({ x: e.clientX, y: e.clientY });
};
return (
<div onMouseMove={handleMouseMove} style={{ height: '100vh' }}>
{children(position)}
</div>
);
}
// 使用
<MouseTracker>
{({ x, y }) => (
<div>
<h1>鼠标位置</h1>
<p>X: {x}, Y: {y}</p>
</div>
)}
</MouseTracker>
八、常用场景教学

本节汇总JSX在日常开发中最常用的场景,每个场景都配有可直接运行的代码示例,方便你上手练习。
8.1 场景一:表单处理
表单是前端开发中最常见的场景之一。JSX配合React的受控组件模式,可以优雅地处理表单:
jsx
import { useState } from 'react';
function RegistrationForm() {
const [formData, setFormData] = useState({
username: '',
email: '',
password: '',
gender: 'male',
hobbies: [],
agree: false
});
const [errors, setErrors] = useState({});
const handleChange = (e) => {
const { name, value, type, checked } = e.target;
if (type === 'checkbox') {
if (name === 'agree') {
setFormData(prev => ({ ...prev, agree: checked }));
} else {
setFormData(prev => ({
...prev,
hobbies: checked
? [...prev.hobbies, value]
: prev.hobbies.filter(h => h !== value)
}));
}
} else {
setFormData(prev => ({ ...prev, [name]: value }));
}
};
const handleSubmit = (e) => {
e.preventDefault();
// 表单验证逻辑
const newErrors = {};
if (!formData.username) newErrors.username = '用户名不能为空';
if (!formData.email) newErrors.email = '邮箱不能为空';
if (formData.password.length < 6) newErrors.password = '密码至少6位';
if (!formData.agree) newErrors.agree = '请同意用户协议';
setErrors(newErrors);
if (Object.keys(newErrors).length === 0) {
console.log('提交数据:', formData);
alert('注册成功!');
}
};
return (
<form onSubmit={handleSubmit} className="registration-form">
<div className="form-group">
<label>用户名</label>
<input
type="text"
name="username"
value={formData.username}
onChange={handleChange}
/>
{errors.username && <span className="error">{errors.username}</span>}
</div>
<div className="form-group">
<label>邮箱</label>
<input
type="email"
name="email"
value={formData.email}
onChange={handleChange}
/>
{errors.email && <span className="error">{errors.email}</span>}
</div>
<div className="form-group">
<label>密码</label>
<input
type="password"
name="password"
value={formData.password}
onChange={handleChange}
/>
{errors.password && <span className="error">{errors.password}</span>}
</div>
<div className="form-group">
<label>性别</label>
<select name="gender" value={formData.gender} onChange={handleChange}>
<option value="male">男</option>
<option value="female">女</option>
<option value="other">其他</option>
</select>
</div>
<div className="form-group">
<label>爱好</label>
<label>
<input
type="checkbox"
name="hobbies"
value="reading"
checked={formData.hobbies.includes('reading')}
onChange={handleChange}
/>
阅读
</label>
<label>
<input
type="checkbox"
name="hobbies"
value="coding"
checked={formData.hobbies.includes('coding')}
onChange={handleChange}
/>
编程
</label>
</div>
<div className="form-group">
<label>
<input
type="checkbox"
name="agree"
checked={formData.agree}
onChange={handleChange}
/>
我已阅读并同意用户协议
</label>
{errors.agree && <span className="error">{errors.agree}</span>}
</div>
<button type="submit">注册</button>
</form>
);
}
8.2 场景二:搜索与筛选
列表页的搜索筛选是后台管理系统中的标配功能:
jsx
import { useState, useMemo } from 'react';
function ProductList({ products }) {
const [searchTerm, setSearchTerm] = useState('');
const [category, setCategory] = useState('all');
const [sortBy, setSortBy] = useState('default');
// 使用useMemo缓存筛选结果,避免每次渲染都重新计算
const filteredProducts = useMemo(() => {
let result = products.filter(product => {
const matchSearch = product.name.toLowerCase().includes(searchTerm.toLowerCase());
const matchCategory = category === 'all' || product.category === category;
return matchSearch && matchCategory;
});
if (sortBy === 'price-asc') {
result = [...result].sort((a, b) => a.price - b.price);
} else if (sortBy === 'price-desc') {
result = [...result].sort((a, b) => b.price - a.price);
} else if (sortBy === 'name') {
result = [...result].sort((a, b) => a.name.localeCompare(b.name));
}
return result;
}, [products, searchTerm, category, sortBy]);
return (
<div className="product-list-page">
{/* 筛选区域 */}
<div className="filter-bar">
<input
type="text"
placeholder="搜索商品..."
value={searchTerm}
onChange={e => setSearchTerm(e.target.value)}
/>
<select value={category} onChange={e => setCategory(e.target.value)}>
<option value="all">全部分类</option>
<option value="electronics">电子产品</option>
<option value="clothing">服装</option>
<option value="food">食品</option>
</select>
<select value={sortBy} onChange={e => setSortBy(e.target.value)}>
<option value="default">默认排序</option>
<option value="price-asc">价格从低到高</option>
<option value="price-desc">价格从高到低</option>
<option value="name">按名称排序</option>
</select>
</div>
{/* 结果统计 */}
<p className="result-count">共找到{filteredProducts.length}件商品</p>
{/* 商品列表 */}
<div className="product-grid">
{filteredProducts.length === 0 ? (
<div className="empty-state">没有找到匹配的商品</div>
) : (
filteredProducts.map(product => (
<div key={product.id} className="product-card">
<img src={product.image} alt={product.name} />
<h3>{product.name}</h3>
<p className="price">¥{product.price}</p>
<span className="category">{product.category}</span>
</div>
))
)}
</div>
</div>
);
}
8.3 场景三:标签页(Tabs)切换
jsx
import { useState } from 'react';
function Tabs({ tabs }) {
const [activeIndex, setActiveIndex] = useState(0);
return (
<div className="tabs-container">
{/* 标签头 */}
<div className="tabs-header">
{tabs.map((tab, index) => (
<button
key={tab.key}
className={`tab-item ${index === activeIndex ? 'active' : ''}`}
onClick={() => setActiveIndex(index)}
>
{tab.label}
</button>
))}
</div>
{/* 标签内容 */}
<div className="tabs-content">
{tabs[activeIndex].content}
</div>
</div>
);
}
// 使用
const tabs = [
{
key: 'basic',
label: '基本信息',
content: (
<div>
<h3>基本信息</h3>
<p>姓名:羽师</p>
<p>职业:前端工程师</p>
</div>
)
},
{
key: 'skills',
label: '技能',
content: (
<div>
<h3>技能列表</h3>
<ul>
<li>React / Vue</li>
<li>TypeScript</li>
<li>Node.js</li>
</ul>
</div>
)
},
{
key: 'contact',
label: '联系方式',
content: (
<div>
<h3>联系方式</h3>
<p>邮箱:example@email.com</p>
<p>电话:138xxxxxxx</p>
</div>
)
}
];
<Tabs tabs={tabs} />
8.4 场景四:模态框(Modal)
jsx
import { useEffect } from 'react';
function Modal({ isOpen, onClose, title, children }) {
// 按ESC键关闭模态框
useEffect(() => {
if (!isOpen) return;
const handleKeyDown = (e) => {
if (e.key === 'Escape') {
onClose();
}
};
document.addEventListener('keydown', handleKeyDown);
// 禁止背景滚动
document.body.style.overflow = 'hidden';
return () => {
document.removeEventListener('keydown', handleKeyDown);
document.body.style.overflow = '';
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return (
<div className="modal-overlay" onClick={onClose}>
<div className="modal-content" onClick={e => e.stopPropagation()}>
<div className="modal-header">
<h3>{title}</h3>
<button className="close-btn" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{children}
</div>
</div>
</div>
);
}
// 使用
function App() {
const [isModalOpen, setIsModalOpen] = useState(false);
return (
<div>
<button onClick={() => setIsModalOpen(true)}>打开模态框</button>
<Modal
isOpen={isModalOpen}
onClose={() => setIsModalOpen(false)}
title="确认删除"
>
<p>确定要删除这条记录吗?此操作不可撤销。</p>
<div className="modal-actions">
<button onClick={() => setIsModalOpen(false)}>取消</button>
<button className="danger" onClick={handleDelete}>确认删除</button>
</div>
</Modal>
</div>
);
}
九、竞品对比:前端模板方案优劣势分析

JSX并非前端模板方案的唯一选择。在当前的前端生态中,主要有四种主流的UI描述方式:React JSX、Vue模板、模板字符串、以及Svelte的模板语法。本节从多个维度进行深度对比,帮助你在技术选型时做出明智的决策。
9.1 四种方案概览
React JSX :JavaScript语法扩展,在JS中书写类XML标记,编译为React.createElement调用。
Vue模板 :基于HTML的模板语法,使用v-if、v-for、v-model等指令,编译为渲染函数。
模板字符串 :原生JavaScript的模板字面量(反引号),通过字符串拼接生成HTML,通常配合innerHTML使用。
Svelte模板:类似Vue的HTML模板语法,但在编译阶段直接生成操作真实DOM的代码,没有虚拟DOM。
9.2 多维度对比表
| 对比维度 | React JSX | Vue模板 | 模板字符串 | Svelte模板 |
|---|---|---|---|---|
| 语法本质 | JavaScript语法扩展,编译为函数调用 | HTML模板,编译为渲染函数 | 原生JS字符串,运行时拼接 | HTML模板,编译为DOM操作 |
| 学习成本 | 中等,需要理解JS表达式与HTML的融合 | 低,接近原生HTML,指令直观 | 极低,就是原生JS | 低,类似Vue但更简洁 |
| 灵活性 | 极高,拥有JS全部能力 | 中等,受模板语法约束,复杂逻辑需用渲染函数 | 极高,但缺乏结构约束 | 中等,类似Vue |
| 类型安全 | 优秀,TSX原生支持完整类型检查 | 良好,Volar支持模板类型检查 | 无,纯字符串无法类型检查 | 良好,基于TypeScript |
| 编译期检查 | 优秀,语法错误编译时报错 | 良好,模板编译期可检测部分错误 | 无,运行时才发现错误 | 优秀,编译期严格检查 |
| XSS防护 | 自动转义,默认安全 | 自动转义,默认安全 | 需手动处理,易出漏洞 | 自动转义,默认安全 |
| 运行时性能 | 良好,虚拟DOM Diff优化 | 良好,响应式系统精准更新 | 较差,全量innerHTML替换 | 优秀,无虚拟DOM开销 |
| 打包体积 | 较大,需包含React运行时 | 中等,需包含Vue运行时 | 最小,无额外依赖 | 极小,编译后几乎无运行时 |
| 代码可读性 | 优秀,结构与逻辑内聚 | 良好,模板结构清晰 | 差,字符串拼接难以阅读 | 良好,模板结构清晰 |
| IDE支持 | 优秀,语法高亮、补全、重构完善 | 优秀,Volar提供完善支持 | 一般,字符串内无智能提示 | 良好,Svelte Language Server |
| 复杂逻辑表达 | 优秀,原生JS直接表达 | 一般,复杂逻辑需computed或渲染函数 | 优秀,但代码混乱 | 一般,类似Vue |
| 适用场景 | 复杂交互应用、企业级中后台、跨平台开发 | 中小型应用、快速原型、内容管理系统 | 简单页面、邮件模板、服务端渲染 | 性能敏感型应用、小型项目、静态站点 |
| 生态成熟度 | 极高,最大的前端生态 | 高,国内生态尤其完善 | 无生态,原生能力 | 中等,快速增长中 |
9.3 各方案优劣势详解
React JSX的优势:
- 灵活性无可匹敌:JSX就是JavaScript,任何能用JS表达的逻辑都能直接写在JSX中,不需要学习额外的模板指令
- 组件化最彻底:结构、逻辑、样式内聚在一个组件中,符合"高内聚低耦合"的设计原则
- 跨平台能力强:同一套JSX语法可用于Web(React)、移动端(React Native)、桌面端(Electron)
- 类型安全最完善:TSX提供从组件属性到DOM属性的全链路类型检查
- 生态最丰富:npm上React相关的组件库和工具数量远超其他框架
React JSX的劣势:
- 初学者门槛较高:需要同时理解JavaScript和类HTML语法,对于只有HTML/CSS基础的新手有一定学习曲线
- 样板代码较多:Hooks、状态管理等需要编写较多代码
- 运行时体积较大:React本身的运行时加上虚拟DOM机制,打包体积相对较大
Vue模板的优势:
- 学习曲线平缓:基于HTML的模板语法对新手友好,有HTML基础就能快速上手
- 响应式系统省心:数据变化自动触发视图更新,不需要手动管理渲染
- 单文件组件清晰:
<template>、<script>、<style>三段式结构,职责分明 - 国内生态强大:中文文档完善,国内企业使用率高
Vue模板的劣势:
- 灵活性受限:模板语法有固定的指令集,复杂逻辑需要借助
computed、watch或渲染函数 - 指令繁多:
v-if、v-for、v-model、v-show、v-slot等需要记忆 - 跨平台能力弱于React:虽然有Vue Native,但成熟度远不及React Native
模板字符串的优势:
- 零依赖:纯原生JavaScript,不需要任何框架或编译器
- 极致灵活:可以在字符串中嵌入任意JS表达式
- 学习成本为零:每个JS开发者都会用模板字符串
模板字符串的劣势:
- 安全性差:默认不转义,XSS风险高
- 可维护性差:字符串中的HTML没有语法高亮、没有格式化、没有智能提示
- 性能低下:每次更新都需要全量替换innerHTML,无法做增量更新
- 无编译期检查:语法错误、拼写错误只能在运行时发现
Svelte模板的优势:
- 性能优异:编译时直接生成DOM操作代码,无虚拟DOM开销
- 打包体积极小:编译后几乎不包含运行时代码
- 语法简洁:比Vue更简洁的模板语法,响应式声明直观
Svelte模板的劣势:
- 生态尚不成熟:组件库和工具链的丰富程度远不及React和Vue
- 大型项目验证不足:在超大规模企业级项目中的实践案例相对较少
- 社区规模较小:遇到问题时可搜索的解决方案较少
9.4 选型建议
基于以上对比,给出以下选型建议:
- 企业级中后台系统、复杂交互应用:首选React JSX,其灵活性和生态优势在复杂场景下无可替代
- 营销页面、内容展示型网站、快速原型:Vue模板是高效之选,开发速度快,上手门槛低
- 性能极度敏感的小型应用、静态站点:Svelte是值得尝试的新选择
- 简单的活动页、邮件模板、服务端渲染:模板字符串可以作为轻量方案,但务必注意XSS防护
需要强调的是,这些方案并非互斥。Vue也支持JSX语法,React也可以通过第三方库使用模板语法。技术选型应该基于团队能力、项目需求和生态现状综合判断,而不是盲目追新。
十、面试官高频面试题

JSX是前端面试中的高频考点,面试官往往通过JSX相关问题来考察你对React底层原理的理解深度。以下是最常被问到的十道面试题,每道都附有详细的参考答案。
面试题一:什么是JSX?它的本质是什么?
参考答案:
JSX(JavaScript XML)是JavaScript的语法扩展,允许在JS代码中书写类XML/HTML的标记语法。
它的本质是React.createElement()的语法糖。JSX在编译阶段会被Babel或TypeScript转换为React.createElement(type, props, children)函数调用,该函数返回一个普通的JavaScript对象(React元素,即虚拟DOM)。
例如:
jsx
const element = <h1 className="title">Hello</h1>;
编译后等价于:
javascript
const element = React.createElement('h1', { className: 'title' }, 'Hello');
面试题二:JSX中的花括号{}里面可以写什么?不能写什么?
参考答案:
花括号中可以写任意合法的JavaScript表达式,包括:
- 变量和常量:
{name} - 运算表达式:
{1 + 2}、{a > b ? '大' : '小'} - 函数调用:
{user.getName()} - 方法调用:
{array.map(item => <li>{item}</li>)} - 三元表达式:
{isLoggedIn ? '欢迎' : '请登录'} - 逻辑表达式:
{count > 0 && <span>{count}</span>} - JSX本身:
{<div>嵌套JSX</div>}
不能写JavaScript语句,包括:
- if/else语句:
{if (x) {...}}(错误) - for/while循环:
{for (...) {...}}(错误) - 变量声明:
{let x = 1}(错误) - 函数声明:
{function foo() {}}(错误)
如果需要条件判断,使用三元表达式或&&;如果需要循环,使用数组的map方法;如果逻辑复杂,可以在JSX外部用变量保存结果。
面试题三:为什么JSX中用className而不是class?用htmlFor而不是for?
参考答案:
因为class和for都是JavaScript的保留字(关键字)。在ES6之前,class就已经是保留字,ES6中正式成为类声明的关键字;for则是循环语句的关键字。
JSX最终会被编译为JavaScript函数调用,属性名作为对象的键传递。如果使用class作为属性名,在某些JavaScript环境中可能会引发语法冲突或解析问题。因此React团队选择使用className和htmlFor来对应HTML中的class和for属性。
这本质上是DOM API的命名规范------在原生JavaScript中,获取元素的class属性也是用element.className,而非element.class。
面试题四:列表渲染中为什么需要key?key的作用是什么?
参考答案:
key是React用于识别列表中每个元素身份的特殊属性。它的核心作用是帮助React的Diff算法高效地更新DOM。
当列表发生变化(增删、排序)时,React通过key来匹配新旧虚拟DOM树中的元素:
- 如果key相同,React认为是同一个元素,只更新其变化的属性
- 如果key不同,React会销毁旧元素、创建新元素
- 如果key缺失,React默认使用数组索引作为key,这在列表动态变化时会导致性能问题和状态错误
使用key的最佳实践:
- key必须是唯一的(在兄弟节点之间)
- key应该是稳定的(不要使用随机数或每次渲染都变化的值)
- 优先使用数据的唯一ID作为key
- 避免使用数组索引作为key(除非列表是静态的、不会增删排序)
使用索引作为key的问题场景:当在列表头部插入新元素时,所有后续元素的索引都会变化,导致React认为所有元素都变了,从而全部重新渲染,并且可能导致输入框等有状态元素的状态错乱。
面试题五:React 17的新JSX转换和旧转换有什么区别?
参考答案:
React 17引入了新的JSX转换,主要区别如下:
旧转换(React 16及之前):
JSX被编译为React.createElement调用,因此每个使用JSX的文件都必须import React:
jsx
// 源码
import React from 'react';
const element = <h1>Hello</h1>;
// 编译后
import React from 'react';
const element = React.createElement('h1', null, 'Hello');
新转换(React 17及之后):
JSX被编译为从react/jsx-runtime导入的jsx函数,不再需要手动import React:
jsx
// 源码(不需要import React)
const element = <h1>Hello</h1>;
// 编译后
import { jsx as _jsx } from 'react/jsx-runtime';
const element = _jsx('h1', { children: 'Hello' });
新转换的好处:
- 不再需要手动
import React,减少了样板代码和因忘记导入导致的运行时错误 - 编译输出更精简,
jsx函数比React.createElement更轻量 - 为未来的React编译器优化奠定了基础
- 允许React团队在不破坏向后兼容的前提下改进
createElement的实现
需要注意的是,新转换是编译工具(Babel/TypeScript)的特性,而非React运行时的特性。你需要配置@babel/preset-react的runtime: 'automatic'选项,或TypeScript的jsx: 'react-jsx'选项来启用。
面试题六:JSX和HTML有什么区别?
参考答案:
JSX虽然看起来像HTML,但有以下关键区别:
- 根元素要求:JSX组件必须返回单个根元素(或用Fragment包裹),HTML没有这个限制
- 标签闭合 :JSX要求所有标签必须闭合,包括自闭合标签(
<img />),HTML中部分标签可以不闭合 - 属性命名 :JSX使用驼峰命名(
className、onClick、tabIndex),HTML使用短横线(class、onclick、tabindex) - style属性 :JSX的style接收对象(
style={``{ color: 'red' }}),HTML的style接收字符串(style="color: red") - 事件绑定 :JSX使用驼峰事件属性+函数引用(
onClick={handleClick}),HTML使用小写事件属性+字符串(onclick="handleClick()") - 表达式嵌入 :JSX通过
{}嵌入JS表达式,HTML中没有原生的表达式嵌入机制 - 注释语法 :JSX注释为
{/* 注释 */},HTML注释为<!-- 注释 --> - 布尔属性 :JSX中
disabled={true}可以简写为disabled,HTML中也是类似但处理机制不同 - 安全性:JSX默认对内容进行HTML转义防止XSS,HTML中插入动态内容需要手动处理
面试题七:JSX中如何实现条件渲染?有哪些方式?
参考答案:
JSX中实现条件渲染主要有以下几种方式:
- if/else语句(组件外部):在return之前根据条件返回不同的JSX
- 三元表达式 :
{condition ? <A /> : <B />},适用于二分支场景 - 逻辑与&& :
{condition && <A />},适用于满足条件才渲染的场景 - 逻辑或|| :
{value || <Default />},适用于提供默认值的场景 - 元素变量:将JSX赋值给变量,根据条件修改变量的值
- IIFE(立即执行函数) :
{(() => { if (...) { return <A />; } else { return <B />; } })()},适用于在JSX内部写复杂逻辑 - switch-case:在组件外部用switch处理多分支条件
注意事项:
&&左侧必须确保是布尔值,{count && <span/>}在count为0时会渲染"0"- 不要在JSX中直接写if语句,因为if是语句不是表达式
- 复杂条件建议提取到组件外部或单独的函数中,保持JSX的可读性
面试题八:JSX是如何被编译成JavaScript的?简述Babel的编译流程。
参考答案:
JSX的编译流程分为三个阶段:
-
解析(Parse):Babel使用解析器(如@babel/parser)将JSX源码解析为AST(抽象语法树)。JSX标签会被解析为特定的AST节点类型(JSXElement)。
-
转换(Transform):Babel的插件(@babel/plugin-transform-react-jsx)遍历AST,将JSX相关的节点转换为函数调用节点。
- 旧转换:将
<div className="x">text</div>转换为React.createElement('div', { className: 'x' }, 'text') - 新转换:转换为
_jsx('div', { className: 'x', children: 'text' }),并自动添加import { jsx as _jsx } from 'react/jsx-runtime'
- 旧转换:将
-
生成(Generate):将转换后的AST重新生成为JavaScript代码字符串。
整个编译过程在构建阶段完成,运行时浏览器执行的已经是纯JavaScript代码,不需要任何JSX解析器。这也是为什么JSX不会带来运行时性能开销的原因。
面试题九:JSX中的Fragment是什么?为什么需要它?
参考答案:
Fragment(片段)是React提供的一个特殊组件,用于在不产生额外DOM节点的情况下包裹多个子元素。
使用方式:
jsx
// 方式一:使用<React.Fragment>
<React.Fragment>
<li>第一项</li>
<li>第二项</li>
</React.Fragment>
// 方式二:使用简写语法<>...</>(React 16.2+支持)
<>
<li>第一项</li>
<li>第二项</li>
</>
需要Fragment的原因:
- JSX要求组件返回单个根元素,但有时候我们不希望在DOM中增加额外的包裹层(比如在
<table>中渲染多个<tr>,或者在<ul>中渲染多个<li>) - 使用
<div>包裹会破坏HTML语义结构,可能导致CSS选择器失效 - Fragment不会在DOM中产生任何节点,只起到逻辑上的包裹作用
Fragment可以带key属性(在列表渲染中),但简写语法<></>不支持任何属性:
jsx
{items.map(item => (
<React.Fragment key={item.id}>
<dt>{item.term}</dt>
<dd>{item.description}</dd>
</React.Fragment>
))}
面试题十:JSX中的事件处理和原生HTML事件有什么区别?
参考答案:
主要区别如下:
- 命名方式 :JSX使用驼峰命名(
onClick、onMouseEnter),HTML使用小写(onclick、onmouseenter) - 事件处理值 :JSX传入函数引用(
onClick={handleClick}),HTML传入函数字符串(onclick="handleClick()") - 事件对象:JSX中的事件对象是React合成事件(SyntheticEvent),是对原生事件的跨浏览器封装;HTML中是原生事件对象
- 事件绑定机制:React采用事件委托机制,将所有事件绑定到根节点(React 17之前是document,React 17之后是root容器),通过事件冒泡统一处理;HTML中事件直接绑定在元素上
- 阻止默认行为 :JSX中必须调用
e.preventDefault(),不能用return false;HTML中return false可以同时阻止默认行为和事件冒泡 - this绑定:JSX中需要注意this指向问题(类组件中),通常用箭头函数或bind绑定;HTML中不存在this指向问题
React合成事件的优势:
- 跨浏览器一致性,屏蔽了不同浏览器的事件API差异
- 事件委托减少了内存占用,提升性能
- 可以在事件系统层面做统一的优化和扩展
结语
从字符串拼接的噩梦,到JSX带来的开发体验革命,前端模板方案的演进折射出整个前端工程化的发展历程。JSX不仅仅是一种语法糖,它代表了一种开发理念的转变------从"按技术栈分离"到"按组件内聚",从"命令式操作DOM"到"声明式描述UI"。
回顾本文的核心要点:
- JSX是JavaScript的语法扩展,本质是
React.createElement的语法糖 - JSX在编译期完成转换,运行时不产生额外开销
- JSX提供了可读性、类型安全、XSS防护等全方位的工程价值
- 从XHP到React Compiler,JSX经历了十几年的演进和优化
- 掌握条件渲染、列表渲染、动态组件等模式,是企业项目实战的必备技能
- 理解JSX的底层原理和编译流程,是通过前端面试的关键
技术的世界日新月异,但底层的设计思想往往一脉相承。希望这篇文章能帮助你不仅"会用"JSX,更能"懂"JSX,在未来的技术选型和架构设计中做出更明智的决策。
转载声明:本文为原创文章,如需转载,请联系作者获得授权,并注明出处。