跳到主要内容
预计阅读 43 分钟

JSX与组件 —— 用积木搭建用户界面

还记得小时候玩乐高积木吗?一块块形状各异的小零件,经过拼接组合,能变成城堡、飞机、甚至整座城市。React 的组件化开发与此异曲同工——每个组件就是一块积木,而 JSX 则是你手中的”拼装说明书”。它让你可以在 JavaScript 代码里直接”画”出界面该长什么样,然后由 React 负责把这些描述变成真实的页面。今天,我们就来揭开 JSX 的面纱,学会用组件这块”积木”搭建用户界面。

📋 开篇自测:

  1. 你能解释 JSX 和 HTML 之间有什么区别吗?
  2. 你知道 JSX 代码在浏览器运行之前经历了怎样的转换过程吗?
  3. 函数组件和类组件,你能说出各自的写法和区别吗?

一、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 的关键区别,初学时特别容易踩坑:

区别点HTMLJSX
class 属性class="box"className="box"
for 属性for="name"htmlFor="name"
样式写法style="color: red"style={{ color: 'red' }}
标签闭合<br> 可以不闭合<br /> 必须闭合
事件名onclickonClick(驼峰命名)

为什么要这么做?因为 JSX 本质上是 JavaScript,而 classfor 都是 JavaScript 的保留关键字,直接使用会产生冲突。所以 React 用 classNamehtmlFor 来代替。

🤔 想一想 既然 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 有两条重要规则:

  1. key 在兄弟节点中必须唯一(不需要全局唯一)。
  2. 尽量不要用数组索引 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,函数组件可以拥有状态管理、副作用处理等完整能力,不再是只能展示静态内容的”无状态组件”。

函数组件的优势显而易见:

  • 写法简洁:没有 classthisrender 等样板代码。
  • 容易理解:输入 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 管理状态,通过各种生命周期方法(componentDidMountcomponentDidUpdate 等)处理副作用。

一个带状态的类组件示例:

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>
  );
}

这种拆分带来了三个好处:

  1. 可复用Avatar 组件可以在个人主页、评论区、聊天列表等任何地方复用。
  2. 可维护:修改点赞按钮的逻辑,只需要改 PostActions,不会影响其他部分。
  3. 可测试:每个小组件都可以单独编写测试用例。

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 接收三类参数:

  1. type:标签类型。如果是原生 HTML 标签,传字符串(如 'div''h1');如果是自定义组件,传组件函数或类本身(如 Welcome)。
  2. props:属性对象。包含标签上的所有属性,没有属性则传 null
  3. 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.idskill 作为 key

这个例子虽然不复杂,但它展示了 React 开发的核心工作流:把界面拆分成小组件,用 props 传递数据,用 JSX 描述结构,最终通过组合构建出完整的应用


📝 掌握度自测

  1. JSX 中 className 和 HTML 中 class 有什么不同?为什么 React 要做这个改变?

  2. 在列表渲染时,为什么不推荐使用数组索引 index 作为 key?请举一个具体的例子说明可能出现的问题。

  3. 请描述 JSX 从编写到最终显示在浏览器上的完整转换链路:JSX 代码经历了哪几步才变成屏幕上的像素?

  4. 函数组件和类组件有哪些主要区别?现代 React 开发为什么推荐使用函数组件?

  5. 给你一段代码 <Card title="标题"><p>内容</p></Card>,请解释 <p>内容</p> 是如何被 Card 组件接收到的?这种模式叫什么?

💡 自我评估

  • 全部答对:你已经扎实掌握了 JSX 和组件的核心概念,可以放心进入下一章了。
  • 答对3-4题:基础掌握得不错,建议回顾一下薄弱的部分,特别是 JSX 编译原理和 key 的作用。
  • 答对1-2题:不用气馁,JSX 和组件是后续所有章节的地基。建议动手把实战部分的代码敲一遍,在实践中加深理解。

购买课程解锁全部内容

从组件到架构:12 章系统掌握现代 React

¥29.90