react

React 元素与 JSX 语法

By AI-Writer 8 min read

前言

React 的核心理念之一是用声明式的方式描述 UI。JSX(JavaScript XML)正是这一理念的具体实现——它允许我们在 JavaScript 中用类似 HTML 的语法来描述界面结构。本文将带你从零理解 JSX 的本质、虚拟 DOM 的工作原理,以及常见错误的规避方法。

JSX 的本质

什么是 JSX

JSX 并不是一种独立的模板语言,而是一种语法扩展。它让我们可以在 JavaScript 中写类似 HTML 的标记:

jsx
// JSX 语法(我们写的)
const element = <h1 className="title">Hello, React!</h1>;

这段代码经过编译后,会变成:

javascript
// 编译后的 JavaScript
const element = React.createElement(
  'h1',
  { className: 'title' },
  'Hello, React!'
);

React.createElement() 的返回值是一个虚拟 DOM 元素(一个普通的 JavaScript 对象),它描述了你希望在屏幕上看到的内容。

React.createElement 的结构

javascript
// createElement(标签名, 属性对象, 子元素...)
React.createElement('div', { id: 'root' },
  React.createElement('h1', null, '标题'),
  React.createElement('p', null, '段落内容')
);

虚拟 DOM 概念

为什么需要虚拟 DOM

操作真实 DOM 是网页性能的主要瓶颈之一。真实 DOM 结构庞大,每次修改都可能触发浏览器的重排(reflow)和重绘(repaint)。

虚拟 DOM 是真实 DOM 的 JavaScript 对象表示。当状态变化时,React 会:

  1. 创建新的虚拟 DOM 树
  2. 与旧虚拟 DOM 树进行对比(Diff 算法)
  3. 仅将变化的最小部分更新到真实 DOM
jsx
// 状态变化 → 重新渲染
function Counter() {
  const [count, setCount] = React.useState(0);

  return (
    <div>
      {/* 每次 count 变化,React 对比新旧虚拟 DOM,仅更新文本节点 */}
      <p>计数:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
    </div>
  );
}

虚拟 DOM 的优势

特点说明
声明式描述「应该是什么」,而非「如何操作 DOM」
跨平台同一套虚拟 DOM 可渲染到 DOM、Native、Canvas 等
批量更新多次状态变化合并为一次真实 DOM 更新
可预测状态 → 虚拟 DOM → 真实 DOM,数据流清晰

JSX 语法规则

HTML 与 JSX 的关键区别

由于 JSX 在 JavaScript 上下文中解析,以下 HTML 特性需要特别注意:

jsx
function SyntaxDifferences() {
  return (
    <div>
      {/* ❌ class 是 JavaScript 保留字,使用 className */}
      <h1 className="heading">标题</h1>

      {/* ❌ label 的 for 属性,使用 htmlFor */}
      <label htmlFor="username">用户名</label>
      <input id="username" type="text" />

      {/* ❌ 自闭合标签必须显式闭合 */}
      <img src="photo.jpg" alt="照片" />
      <input type="text" value="hello" />

      {/* ✓ style 接收一个对象(camelCase 属性名) */}
      <div style={{ color: 'red', fontSize: '16px', backgroundColor: '#f0f0f0' }}>
        内联样式
      </div>

      {/* ✓ 注释用花括号包裹 */}
      {/*
        多行注释示例
        注意:这是 JSX 中的注释语法
      */}
    </div>
  );
}

在 JSX 中嵌入表达式

JSX 中可以嵌入任意 JavaScript 表达式,用 {} 包裹:

jsx
function Expressions() {
  const name = 'Bauhaus';
  const isLoggedIn = true;
  const styles = { color: '#D02020' };

  return (
    <div>
      {/* 基本变量 */}
      <p>欢迎,{name}!</p>

      {/* 表达式计算 */}
      <p>2 + 2 = {2 + 2}</p>

      {/* 三元运算符 */}
      <p>{isLoggedIn ? '已登录' : '未登录'}</p>

      {/* 函数调用 */}
      <p>时间:{new Date().toLocaleTimeString()}</p>

      {/* 逻辑与运算符:条件渲染 */}
      {isLoggedIn && <button>登出</button>}

      {/* 应用样式对象 */}
      <span style={styles}>红色文本</span>
    </div>
  );
}

标签必须闭合

JSX 语法要求所有标签必须正确闭合:

jsx
// ❌ 错误
const bad = <div><input></div>;

// ✓ 正确 — 自闭合标签
const good1 = <input type="text" />;

// ✓ 正确 — 配对闭合标签
const good2 = (
  <div>
    <h1>标题</h1>
    <p>段落</p>
  </div>
);

单一根元素

JSX 组件必须返回单一根元素。可用** Fragment**(<>...</>)包裹多个元素:

jsx
// ❌ 错误:多个同级元素
function BadComponent() {
  return (
    <h1>标题</h1>
    <p>段落</p>
  );
}

// ✓ 正确:用 div 包裹
function GoodComponent1() {
  return (
    <div>
      <h1>标题</h1>
      <p>段落</p>
    </div>
  );
}

// ✓ 正确:用 Fragment 包裹(不产生额外 DOM 节点)
function GoodComponent2() {
  return (
    <>
      <h1>标题</h1>
      <p>段落</p>
    </>
  );
}

JSX 编译原理

现代 React 项目通常使用 Vite 或 Create React App 创建,它们内置了 Babel 来处理 JSX 编译。

Babel 转换过程

plaintext
JSX 源码
  ↓ Babel(@babel/plugin-transform-react-jsx)
React.createElement 调用
  ↓ React 运行时
虚拟 DOM 对象(React Element)
  ↓ React DOM(render)
真实 DOM 节点

React 17 的新 JSX 转换

React 17 引入了全新的 JSX 转换器,不再需要手动导入 React

jsx
// React 17+:新的 JSX 转换
// 不再需要 import React from 'react';
// Babel 自动导入以下运行时函数

// 旧转换:React.createElement('div', null, ...)
// 新转换:jsx('div', { children: ... }) — 来自 react/jsx-runtime
jsx
// App.jsx(React 17+ 项目)
// 无需 import React from 'react'

function App() {
  // 这段 JSX 会被 Babel 转换为 jsx() 调用
  return <div className="app">Hello World</div>;
}

export default App;

常见错误与排查

1. Falsy 值渲染问题

jsx
function FalsyDemo() {
  const count = 0;

  return (
    <div>
      {/* ❌ count 为 0,不会渲染任何内容,但 0 本身是合法的 falsy 值 */}
      <p>{count && <span>计数:{count}</span>}</p>
      {/* 输出: 0(数字 0 本身会被渲染) */}

      {/* ✓ 推荐:使用三元运算符明确处理 */}
      <p>{count > 0 ? `计数:${count}` : '无计数'}</p>
    </div>
  );
}

2. 对象不能作为文本渲染

jsx
function ObjectError() {
  const user = { name: 'Alice', age: 28 };

  return (
    <div>
      {/* ❌ 报错:对象不能作为 React 子元素渲染 */}
      <p>{user}</p>

      {/* ✓ 正确:提取对象的属性 */}
      <p>{user.name},{user.age}岁</p>
    </div>
  );
}

3. 事件处理器未定义

jsx
function EventError() {
  function handleClick() {
    console.log('点击了');
  }

  return (
    <div>
      {/* ❌ 忘记绑定 this 或传入函数 */}
      <button onClick={handleClick()}>错误写法</button>

      {/* ✓ 正确:传入函数引用 */}
      <button onClick={handleClick}>正确写法</button>
    </div>
  );
}

完整示例:声明式计数器

让我们用一个完整示例串联所有概念:

jsx
import React from 'react';

function Counter() {
  // 响应式状态
  const [count, setCount] = React.useState(0);
  const [step, setStep] = React.useState(1);

  // 事件处理函数
  function increment() {
    setCount(count + step);
  }

  function decrement() {
    setCount(count - step);
  }

  function reset() {
    setCount(0);
  }

  // 派生值(不需要 state,因为可以从现有 state 计算)
  const isPositive = count > 0;
  const isNegative = count < 0;
  const status = count === 0 ? '归零' : isPositive ? '正数' : '负数';

  return (
    <div className="counter" style={{ padding: '1rem', maxWidth: '300px' }}>
      <h2>计数器 — {status}</h2>

      {/* 表达式嵌入 */}
      <div className="display" style={{ fontSize: '3rem', fontWeight: 'bold', textAlign: 'center' }}>
        {count}
      </div>

      {/* 步进控制 */}
      <div className="step-control" style={{ marginTop: '0.5rem' }}>
        <label>
          步进值:
          <input
            type="number"
            value={step}
            onChange={(e) => setStep(Number(e.target.value))}
            style={{ marginLeft: '0.5rem', width: '60px' }}
          />
        </label>
      </div>

      {/* 按钮组 */}
      <div className="buttons" style={{ display: 'flex', gap: '0.5rem', justifyContent: 'center', marginTop: '1rem' }}>
        <button onClick={decrement}>-</button>
        <button onClick={reset} style={{ backgroundColor: '#F0C020', color: '#121212' }}>重置</button>
        <button onClick={increment}>+</button>
      </div>
    </div>
  );
}

export default Counter;

小结

  • JSX 本质:JavaScript 语法扩展,经 Babel 编译为 React.createElement() 调用,返回虚拟 DOM 对象
  • 虚拟 DOM:JavaScript 对象树,通过 Diff 算法实现高效更新,只变更必要部分
  • 语法要点classNamehtmlFor、自闭合标签、单一根元素(Fragment)
  • 嵌入表达式{} 中可写任意 JS 表达式,包括变量、计算、三元、函数调用
  • React 17+:新的 JSX 运行时无需每个文件导入 React

掌握 JSX 和虚拟 DOM 的核心概念后,你已经理解了 React 声明式 UI 的基础。下一篇文章我们将深入探讨组件与 Props,了解如何构建可复用的 UI 模块。

#react #jsx #入门

评论

A

Written by

AI-Writer

Related Articles

react
#3

State 与 setState 机制

深入理解 useState Hook 的工作原理,掌握状态更新的异步性、批量更新机制、函数式更新以及状态提升模式

Read More
react
#7

useRef 与 DOM 操作

深入理解 useRef 的工作原理,掌握 Ref 对象操作、可变引用的使用场景,以及 forwardRef 的高级用法

Read More