JSX与组件 —— 用积木搭建用户界面
还记得小时候玩乐高积木吗?一块块形状各异的小零件,经过拼接组合,能变成城堡、飞机、甚至整座城市。React 的组件化开发与此异曲同工——每个组件就是一块积木,而 JSX 则是你手中的”拼装说明书”。它让你可以在 JavaScript 代码里直接”画”出界面该长什么样,然后由 React 负责把这些描述变成真实的页面。今天,我们就来揭开 JSX 的面纱,学会用组件这块”积木”搭建用户界面。
📋 开篇自测:
- 你能解释 JSX 和 HTML 之间有什么区别吗?
- 你知道 JSX 代码在浏览器运行之前经历了怎样的转换过程吗?
- 函数组件和类组件,你能说出各自的写法和区别吗?
一、JSX 是什么?—— 在 JavaScript 里写 HTML 的魔法
打开任何一个 React 项目,你几乎都会看到这样的代码:
function App() {
return <h1>你好,React!</h1>;
}
等一下,JavaScript 文件里怎么混进了 HTML?这不是语法错误吗?
别担心,这就是 JSX(JavaScript XML)。它是 React 团队设计的一种语法扩展,让你可以在 JavaScript 代码中直接书写类似 HTML 的标签结构。注意,我说的是”类似 HTML”而不是”就是 HTML”——它们之间有一些重要的差别,后面会详细讲到。
1.1 为什么需要 JSX
在 JSX 出现之前,如果你想用 JavaScript 创建一个界面元素,需要写成这样:
// 不使用 JSX,纯 JavaScript 写法
const element = React.createElement(
'h1',
{ className: 'greeting' },
'你好,React!'
);
看起来还行?那如果界面结构复杂一点呢:
// 不使用 JSX,构建一个简单的卡片
const card = React.createElement(
'div',
{ className: 'card' },
React.createElement('img', { src: 'avatar.jpg', alt: '头像' }),
React.createElement(
'div',
{ className: 'info' },
React.createElement('h2', null, '张三'),
React.createElement('p', null, '前端工程师')
)
);
层层嵌套的 React.createElement 调用,读起来像在解一道括号匹配题——非常痛苦。而用 JSX 写同样的结构:
const card = (
<div className="card">
<img src="avatar.jpg" alt="头像" />
<div className="info">
<h2>张三</h2>
<p>前端工程师</p>
</div>
</div>
);
一目了然。JSX 的核心价值就在于此:它让 UI 的结构和 JavaScript 的逻辑共存于同一个文件中,既保留了 JavaScript 的全部能力,又拥有 HTML 般的直观表达。
你可以把 JSX 想象成一种”语法糖”——它不会增加新的功能,但会让原有的写法变得更甜蜜、更易读。最终,所有的 JSX 都会被编译工具(通常是 Babel)转换成 React.createElement 调用,浏览器运行的依然是纯粹的 JavaScript。
1.2 JSX 不是 HTML
虽然长得像,但 JSX 有几处和 HTML 的关键区别,初学时特别容易踩坑:
| 区别点 | HTML | JSX |
|---|---|---|
| class 属性 | class="box" | className="box" |
| for 属性 | for="name" | htmlFor="name" |
| 样式写法 | style="color: red" | style={{ color: 'red' }} |
| 标签闭合 | <br> 可以不闭合 | <br /> 必须闭合 |
| 事件名 | onclick | onClick(驼峰命名) |
为什么要这么做?因为 JSX 本质上是 JavaScript,而 class 和 for 都是 JavaScript 的保留关键字,直接使用会产生冲突。所以 React 用 className 和 htmlFor 来代替。
🤔 想一想 既然 JSX 最终会被编译成
React.createElement调用,那是不是意味着我们可以完全不写 JSX,只用React.createElement?这样做有什么优缺点?
二、JSX 语法规则 —— 表达式、条件渲染与列表渲染
JSX 的强大之处在于,你可以在标签结构中自由地嵌入 JavaScript 表达式。只需要用一对大括号 {} 把表达式包起来,就像给 JavaScript 开了一扇窗。
2.1 嵌入表达式
function Greeting() {
const name = '小明';
const hour = new Date().getHours();
return (
<div>
<h1>你好,{name}!</h1>
<p>现在是 {hour} 点,{hour < 12 ? '上午好' : '下午好'}。</p>
<p>1 + 1 = {1 + 1}</p>
</div>
);
}
大括号里可以放什么?记住一个原则:任何合法的 JavaScript 表达式都可以。变量、函数调用、三元运算、算术运算,统统没问题。但注意,if 语句、for 循环这些是”语句”而不是”表达式”,不能直接放在大括号里。
2.2 条件渲染
实际开发中,根据不同条件显示不同内容是家常便饭。JSX 提供了几种优雅的写法:
方式一:三元运算符 —— 适合二选一的场景
function LoginStatus({ isLoggedIn }) {
return (
<div>
{isLoggedIn ? (
<h1>欢迎回来!</h1>
) : (
<h1>请先登录</h1>
)}
</div>
);
}
方式二:逻辑与运算符 && —— 适合”有就显示,没有就隐藏”
function Notification({ messages }) {
return (
<div>
<h1>通知中心</h1>
{messages.length > 0 && (
<p>你有 {messages.length} 条未读消息</p>
)}
</div>
);
}
这里利用了 JavaScript 的短路求值特性:&& 左边为 true 时才执行右边的渲染,左边为 false 时整个表达式返回 false,React 会忽略 false 不渲染任何内容。
方式三:提前 return —— 适合复杂的多分支逻辑
function UserPanel({ user }) {
if (!user) {
return <p>加载中...</p>;
}
if (user.role === 'admin') {
return <AdminDashboard user={user} />;
}
return <UserDashboard user={user} />;
}
当条件分支超过两个,三元运算就会嵌套得面目全非。这时候把判断逻辑放在 return 之前,代码反而更清晰。
2.3 列表渲染
渲染一组数据,用 map 方法最为自然:
function FruitList() {
const fruits = [
{ id: 1, name: '苹果' },
{ id: 2, name: '香蕉' },
{ id: 3, name: '橘子' },
{ id: 4, name: '葡萄' },
];
return (
<ul>
{fruits.map((fruit) => (
<li key={fruit.id}>{fruit.name}</li>
))}
</ul>
);
}
这里有一个关键细节:每个列表项都需要一个 key 属性。key 是 React 用来追踪列表项身份的标识,当列表发生变化时,React 通过 key 来判断哪些项是新增的、删除的还是移动的,从而做出最小化的 DOM 更新。
关于 key 有两条重要规则:
- key 在兄弟节点中必须唯一(不需要全局唯一)。
- 尽量不要用数组索引
index作为 key。如果列表项会被重新排序、增删,索引会导致 React 错误地复用元素,产生难以排查的 Bug。最好使用数据中天然的唯一标识(比如 id)。
function UserList({ users }) {
return (
<ul>
{users.map((user) => (
<li key={user.id}>
{user.name} - {user.email}
</li>
))}
</ul>
);
}
2.4 样式绑定
在 JSX 中绑定样式有两种主要方式:
行内样式:传入一个 JavaScript 对象,属性名使用驼峰命名。
function StyledBox() {
const boxStyle = {
backgroundColor: '#f0f0f0',
padding: '20px',
borderRadius: '8px',
fontSize: '16px',
};
return <div style={boxStyle}>我是一个有样式的盒子</div>;
}
注意 style 接收的是一个对象,不是字符串。写在 JSX 里就是双层大括号——外层是 JSX 表达式的入口,内层是对象字面量:
<div style={{ color: 'red', marginTop: '10px' }}>红色文字</div>
CSS 类名:用 className 引用外部样式表。
// App.css
// .highlight { color: orange; font-weight: bold; }
function HighlightText() {
return <span className="highlight">重要内容</span>;
}
实际项目中,行内样式适合动态变化的场景(比如根据数据计算宽度),静态样式还是写在 CSS 文件中更合适。
三、组件的两种写法 —— 函数组件 vs 类组件
组件是 React 的核心概念。如果说 JSX 是搭建界面的”图纸”,那组件就是可以重复使用的”预制积木块”。你定义一次组件,就可以在任何需要的地方反复使用它。
React 的组件有两种写法:函数组件和类组件。
3.1 函数组件(推荐)
函数组件是当前 React 开发的主流选择。它就是一个普通的 JavaScript 函数,接收 props 参数,返回 JSX:
function Welcome({ name }) {
return <h1>你好,{name}!</h1>;
}
// 也可以用箭头函数
const Welcome = ({ name }) => {
return <h1>你好,{name}!</h1>;
};
使用时像 HTML 标签一样书写:
function App() {
return (
<div>
<Welcome name="小明" />
<Welcome name="小红" />
<Welcome name="小刚" />
</div>
);
}
函数组件之所以成为主流,是因为 React 16.8 引入了 Hooks(后续章节会深入学习)。有了 Hooks,函数组件可以拥有状态管理、副作用处理等完整能力,不再是只能展示静态内容的”无状态组件”。
函数组件的优势显而易见:
- 写法简洁:没有
class、this、render等样板代码。 - 容易理解:输入 props,输出 JSX,就像数学中的函数
f(x) = y。 - 便于测试:纯函数天然易于单元测试。
- 与 Hooks 完美搭配:React 官方的未来方向就是函数组件 + Hooks。
3.2 类组件(了解即可)
类组件是 React 早期的组件写法,使用 ES6 的 class 语法,继承自 React.Component:
import React, { Component } from 'react';
class Welcome extends Component {
render() {
return <h1>你好,{this.props.name}!</h1>;
}
}
类组件通过 this.props 访问属性,通过 this.state 管理状态,通过各种生命周期方法(componentDidMount、componentDidUpdate 等)处理副作用。
一个带状态的类组件示例:
class Counter extends Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return (
<div>
<p>当前计数:{this.state.count}</p>
<button onClick={this.handleClick}>+1</button>
</div>
);
}
}
同样的功能,用函数组件 + Hooks 写起来要简洁得多:
import { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<p>当前计数:{count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
React 官方文档已经将类组件归入”legacy(历史遗留)“分类。除非你需要维护旧代码中的类组件,否则新项目中请直接使用函数组件。本课程后续章节也将以函数组件为主。
🤔 想一想 既然函数组件是主流方向,React 团队为什么没有直接废弃类组件?如果你正在维护一个大型项目,里面既有类组件也有函数组件,你会制定怎样的迁移策略?
四、组件的组合与嵌套 —— 从小积木到大建筑
乐高积木的魅力不在于单块积木有多精美,而在于它们可以无限组合。React 组件也是如此——你不需要写一个巨大的组件来完成所有事情,而是把界面拆分成一个个小组件,然后像搭积木一样把它们组合起来。
4.1 组件拆分的思维方式
假设你要构建一个社交媒体的帖子列表。初学者可能会写出这样一个”万能组件”:
// 不推荐:一个组件干了所有的事
function PostPage() {
return (
<div className="page">
<div className="header">
<img src="logo.png" alt="logo" />
<nav>...</nav>
</div>
<div className="post-list">
<div className="post">
<div className="author">
<img src="avatar1.jpg" />
<span>张三</span>
</div>
<p>今天天气真好...</p>
<div className="actions">
<button>点赞</button>
<button>评论</button>
</div>
</div>
{/* 更多帖子... */}
</div>
</div>
);
}
更好的做法是按职责拆分成多个小组件:
function Avatar({ src, name }) {
return <img className="avatar" src={src} alt={name} />;
}
function AuthorInfo({ author }) {
return (
<div className="author">
<Avatar src={author.avatar} name={author.name} />
<span>{author.name}</span>
</div>
);
}
function PostActions() {
return (
<div className="actions">
<button>点赞</button>
<button>评论</button>
</div>
);
}
function PostCard({ post }) {
return (
<div className="post">
<AuthorInfo author={post.author} />
<p>{post.content}</p>
<PostActions />
</div>
);
}
function PostList({ posts }) {
return (
<div className="post-list">
{posts.map((post) => (
<PostCard key={post.id} post={post} />
))}
</div>
);
}
这种拆分带来了三个好处:
- 可复用:
Avatar组件可以在个人主页、评论区、聊天列表等任何地方复用。 - 可维护:修改点赞按钮的逻辑,只需要改
PostActions,不会影响其他部分。 - 可测试:每个小组件都可以单独编写测试用例。
4.2 children:组件的”插槽”
有时候,你想让组件成为一个”容器”,它不关心里面放什么内容,只提供外层的包装。这时候就要用到 children:
function Card({ title, children }) {
return (
<div className="card">
<div className="card-header">
<h3>{title}</h3>
</div>
<div className="card-body">
{children}
</div>
</div>
);
}
// 使用时,开闭标签之间的内容就是 children
function App() {
return (
<div>
<Card title="个人信息">
<p>姓名:李四</p>
<p>职业:设计师</p>
</Card>
<Card title="最近动态">
<ul>
<li>发布了一篇文章</li>
<li>更新了头像</li>
</ul>
</Card>
</div>
);
}
children 就像是组件留出的一个”插槽”,使用者可以往里面塞入任何内容。这是 React 中实现灵活组件设计的重要模式。
4.3 组件树:层层嵌套的结构
当你把组件一层层组合起来,就形成了一棵”组件树”。整个 React 应用本质上就是一棵以 App 为根节点的组件树:
App
├── Header
│ ├── Logo
│ └── NavBar
├── PostList
│ ├── PostCard
│ │ ├── AuthorInfo
│ │ │ └── Avatar
│ │ └── PostActions
│ ├── PostCard
│ │ ├── AuthorInfo
│ │ │ └── Avatar
│ │ └── PostActions
│ └── ...
└── Footer
数据在这棵树中自顶向下流动(通过 props),就像水从树根流向枝叶。这种单向数据流是 React 架构设计的核心理念,让应用的数据走向清晰可追踪。
五、JSX 的编译原理 —— Babel 如何将 JSX 变成 createElement 调用
我们已经知道 JSX 不是合法的 JavaScript,浏览器无法直接执行它。那它是怎么”变成”浏览器能运行的代码的呢?答案是编译。
5.1 经典模式:JSX → React.createElement
在构建项目时,Babel(一个 JavaScript 编译器)会介入工作。它通过 @babel/plugin-transform-react-jsx 插件,把每一段 JSX 转换成 React.createElement 的函数调用。
比如你写的代码是这样的:
function Hello() {
return (
<div className="greeting">
<h1>你好</h1>
<p>欢迎学习 React</p>
</div>
);
}
经过 Babel 编译后变成:
function Hello() {
return React.createElement(
'div',
{ className: 'greeting' },
React.createElement('h1', null, '你好'),
React.createElement('p', null, '欢迎学习 React')
);
}
React.createElement 接收三类参数:
- type:标签类型。如果是原生 HTML 标签,传字符串(如
'div'、'h1');如果是自定义组件,传组件函数或类本身(如Welcome)。 - props:属性对象。包含标签上的所有属性,没有属性则传
null。 - children:子元素。可以是字符串、其他
React.createElement调用,或者它们的组合。
这就是为什么在 React 17 之前,即使你没有在代码中直接使用 React,也必须在文件顶部写 import React from 'react'——因为编译后的代码中会调用 React.createElement,如果没有导入就会报错。
5.2 新模式:自动引入 JSX 运行时
从 React 17 开始,引入了全新的 JSX Transform(Automatic Runtime)。编译后的代码不再需要 React.createElement,而是使用 react/jsx-runtime 中的函数:
// 你写的代码
function Hello() {
return (
<div className="greeting">
<h1>你好</h1>
<p>欢迎学习 React</p>
</div>
);
}
// 编译后(Automatic Runtime)
import { jsxs as _jsxs, jsx as _jsx } from 'react/jsx-runtime';
function Hello() {
return _jsxs('div', {
className: 'greeting',
children: [
_jsx('h1', { children: '你好' }),
_jsx('p', { children: '欢迎学习 React' }),
],
});
}
最大的变化是:你不需要再手动导入 React 了,编译工具会自动帮你注入必要的运行时函数。如果你用的是 Create React App 4.0+、Vite 或者 Next.js 等现代脚手架,默认就是这种模式。
5.3 从 createElement 到虚拟 DOM
React.createElement 或 _jsx 执行后,返回的是一个普通的 JavaScript 对象,也就是大名鼎鼎的**虚拟 DOM(Virtual DOM)**节点:
// React.createElement('h1', { className: 'title' }, '你好') 的返回值
{
type: 'h1',
props: {
className: 'title',
children: '你好'
},
key: null,
ref: null,
// ...其他内部属性
}
这个对象就是对真实 DOM 的一种”描述”或”蓝图”。React 在内存中维护着一棵由这些对象组成的虚拟 DOM 树。当状态发生变化时,React 会生成一棵新的虚拟 DOM 树,与旧树进行比较(这个过程叫做 Reconciliation / 调和),找出差异,然后只更新需要变化的真实 DOM 节点。
整个流程可以概括为:
JSX → React.createElement / _jsx → 虚拟 DOM 对象 → 真实 DOM
这就是 React 高效更新界面的秘密:不直接操作真实 DOM,而是通过比较虚拟 DOM 来计算最小更新量。就像装修房子,不需要每次都把墙推倒重建,而是对比新旧设计图,只改需要改的地方。
🤔 想一想 既然虚拟 DOM 本质上就是一个 JavaScript 对象,那你觉得 React 的”虚拟 DOM 比直接操作 DOM 更快”这种说法准确吗?在什么场景下这种说法成立,在什么场景下又不成立?
六、实战:搭建一个用户信息卡片组件
学了这么多理论,让我们动手实践!我们来构建一个完整的用户信息卡片组件,把本章学到的知识串联起来。
6.1 需求分析
我们要实现一个用户卡片,它包含以下功能:
- 展示用户头像、姓名、职位
- 展示技能标签列表
- 根据是否在线显示不同的状态指示器
- 支持组件复用,可以渲染多个用户卡片
6.2 逐步实现
首先创建最基础的子组件:
// Avatar 组件 —— 展示用户头像
function Avatar({ src, name, isOnline }) {
return (
<div className="avatar-wrapper">
<img
src={src}
alt={name}
style={{
width: '80px',
height: '80px',
borderRadius: '50%',
objectFit: 'cover',
}}
/>
{isOnline && (
<span
style={{
display: 'inline-block',
width: '12px',
height: '12px',
backgroundColor: '#22c55e',
borderRadius: '50%',
border: '2px solid white',
position: 'absolute',
bottom: '2px',
right: '2px',
}}
/>
)}
</div>
);
}
然后创建技能标签组件:
// SkillTag 组件 —— 单个技能标签
function SkillTag({ name }) {
return (
<span
style={{
display: 'inline-block',
padding: '4px 12px',
margin: '4px',
backgroundColor: '#e0f2fe',
color: '#0277bd',
borderRadius: '16px',
fontSize: '13px',
}}
>
{name}
</span>
);
}
// SkillList 组件 —— 技能标签列表
function SkillList({ skills }) {
if (!skills || skills.length === 0) {
return <p style={{ color: '#999' }}>暂无技能标签</p>;
}
return (
<div style={{ marginTop: '12px' }}>
{skills.map((skill) => (
<SkillTag key={skill} name={skill} />
))}
</div>
);
}
接下来组合成完整的卡片:
// UserCard 组件 —— 完整的用户信息卡片
function UserCard({ user }) {
return (
<div
style={{
position: 'relative',
width: '300px',
padding: '24px',
borderRadius: '12px',
boxShadow: '0 2px 12px rgba(0, 0, 0, 0.1)',
backgroundColor: '#ffffff',
textAlign: 'center',
}}
>
<Avatar
src={user.avatar}
name={user.name}
isOnline={user.isOnline}
/>
<h2 style={{ margin: '12px 0 4px', fontSize: '20px' }}>
{user.name}
</h2>
<p style={{ margin: 0, color: '#666', fontSize: '14px' }}>
{user.title}
</p>
<SkillList skills={user.skills} />
<div
style={{
marginTop: '16px',
padding: '8px',
borderTop: '1px solid #f0f0f0',
color: user.isOnline ? '#22c55e' : '#999',
fontSize: '13px',
}}
>
{user.isOnline ? '在线' : '离线'}
</div>
</div>
);
}
最后在 App 中使用多个卡片:
function App() {
const users = [
{
id: 1,
name: '张三',
title: '高级前端工程师',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=zhangsan',
skills: ['React', 'TypeScript', 'Node.js'],
isOnline: true,
},
{
id: 2,
name: '李四',
title: 'UI 设计师',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=lisi',
skills: ['Figma', 'Sketch', 'CSS'],
isOnline: false,
},
{
id: 3,
name: '王五',
title: '全栈开发者',
avatar: 'https://api.dicebear.com/7.x/avataaars/svg?seed=wangwu',
skills: ['React', 'Go', 'PostgreSQL', 'Docker'],
isOnline: true,
},
];
return (
<div
style={{
display: 'flex',
gap: '24px',
padding: '40px',
backgroundColor: '#f5f5f5',
minHeight: '100vh',
justifyContent: 'center',
alignItems: 'flex-start',
flexWrap: 'wrap',
}}
>
{users.map((user) => (
<UserCard key={user.id} user={user} />
))}
</div>
);
}
export default App;
6.3 回顾与总结
在这个实战中,我们运用了本章的几乎所有知识点:
- JSX 语法:在 JavaScript 中书写 UI 结构
- 表达式嵌入:
{user.name}动态展示数据 - 条件渲染:
isOnline && <span>...</span>控制在线状态显示 - 列表渲染:
users.map(...)和skills.map(...)渲染列表 - 样式绑定:
style={{ ... }}行内样式 - 函数组件:每个组件都是独立的函数
- 组件组合:
Avatar+SkillList组合成UserCard - props 传递:父组件通过属性向子组件传递数据
- key 属性:列表渲染中使用
user.id和skill作为 key
这个例子虽然不复杂,但它展示了 React 开发的核心工作流:把界面拆分成小组件,用 props 传递数据,用 JSX 描述结构,最终通过组合构建出完整的应用。
📝 掌握度自测
-
JSX 中
className和 HTML 中class有什么不同?为什么 React 要做这个改变? -
在列表渲染时,为什么不推荐使用数组索引
index作为key?请举一个具体的例子说明可能出现的问题。 -
请描述 JSX 从编写到最终显示在浏览器上的完整转换链路:JSX 代码经历了哪几步才变成屏幕上的像素?
-
函数组件和类组件有哪些主要区别?现代 React 开发为什么推荐使用函数组件?
-
给你一段代码
<Card title="标题"><p>内容</p></Card>,请解释<p>内容</p>是如何被 Card 组件接收到的?这种模式叫什么?
💡 自我评估
- 全部答对:你已经扎实掌握了 JSX 和组件的核心概念,可以放心进入下一章了。
- 答对3-4题:基础掌握得不错,建议回顾一下薄弱的部分,特别是 JSX 编译原理和 key 的作用。
- 答对1-2题:不用气馁,JSX 和组件是后续所有章节的地基。建议动手把实战部分的代码敲一遍,在实践中加深理解。
购买课程解锁全部内容
从组件到架构:12 章系统掌握现代 React
¥29.90