陌上人如玉
公子世无双

1.1 JSX 语法与本质

1.1 JSX 语法与本质

一句话:JSX 是 React.createElement()(React 17+ 是 jsx() 运行时)的语法糖,编译后返回一个普通的 JavaScript 对象(React Element);它既不是字符串也不是 HTML,因此有自己的一套约束。

一、JSX 是什么

// 你写的
const element = 

Hello, {name}!

; // React 17 之前(经典运行时)编译为: const element = React.createElement( 'h1', { className: 'title' }, 'Hello, ', name, '!' ); // React 17+(自动运行时)编译为: import { jsx as _jsx, jsxs as _jsxs } from 'react/jsx-runtime'; const element = _jsx('h1', { className: 'title', children: ['Hello, ', name, '!'] });

编译产物就是一个对象:

// 上面 element 的真实结构(简化)
{
  $$typeof: Symbol(react.element),   // ⭐ 标识这是 React Element(防 XSS 的结构)
  type: 'h1',
  key: null,
  ref: null,
  props: {
    className: 'title',
    children: ['Hello, ', name, '!'],
  },
}

为什么要用 $$typeof:早期 React 允许从服务端返回 JSON.parse 出来的对象直接渲染,存在被注入「伪造 Element」的风险(比如注入一个 type: 'script')。用 Symbol 标识后,JSON 无法伪造 Symbol,从而堵住这个 XSS 途径。

自动运行时(Automatic Runtime)带来的变化:

// React 17 之前:每个文件都要显式导入 React
import React from 'react';
const el = 
; // React 17+:不需要导入 React,编译器自动注入 jsx 运行时 const el =
;
维度 经典运行时 自动运行时(React 17+)
是否需要 import React ✅ 必须 ❌ 不需要(除非用到 React.xxx)
编译产物 React.createElement jsx / jsxs(从 react/jsx-runtime)
配置 默认 jsx: 'react-jsx'(Vite/TS 默认)
性能 略差(运行时判断参数) 略优(jsx/jsxs 分开处理静态子节点)

二、表达式嵌入:{ } 里能写什么

const name = '张三';
const list = [1, 2, 3];

// ✅ 可以写:任何「表达式」
{name} {/* 变量 */} {1 + 2} {/* 运算 */} {name.toUpperCase()} {/* 函数调用 */} {list.map((x) =>
  • {x}
  • )} {/* 数组 */} {flag ? : } {/* 三元 */} {user?.name} {/* 可选链 */} {list.length > 0 && } {/* 逻辑与 */}
    // ❌ 不能写:语句
    // 需要写逻辑时,把计算提到 JSX 外面
    function Component({ list, flag }) {
      const filtered = list.filter((x) => x.active);      // ✅ 在函数体里算
      const total = list.reduce((a, b) => a + b.price, 0);
    
      // 或者用 IIFE(不推荐,可读性差)
      // {(() => { if (flag) return ; return ; })()}
    
      return (
        
    {filtered.map((x) => )} {total}
    ); }

    三种值的渲染规则:

    值 渲染结果
    字符串、数字 直接显示
    null / undefined / false / true 什么都不渲染(可用于条件渲染)
    数组 逐个渲染(每项需要 key)
    普通对象 ❌ 报错(Objects are not valid as a React child)
    // ⚠️ 常见的「对象渲染报错」
    const user = { name: '张三' };
    return 
    {user}
    ; // ❌ 报错 return
    {user.name}
    ; // ✅ return
    {JSON.stringify(user)}
    ; // ✅(如果只是想展示)

    三、条件渲染的写法

    // ① 三元(最常用,适合二选一)
    {isLogin ?  : }
    
    // ② 逻辑与(只关心「有或没有」)
    {list.length > 0 && }
    
    // ③ 提前 return(组件级条件渲染,可读性最好)
    function Page({ loading, data }) {
      if (loading) return ;
      if (!data) return ;
      return ;
    }
    
    // ④ 变量提取
    let content;
    if (loading) content = ;
    else if (error) content = ;
    else content = ;
    return 
    {content}
    ; // ⑤ 映射表(多分支场景最清晰) const views = { loading: , error: , empty: , success: , }; return
    {views[status]}
    ;

    ⚠️ && 的 0 陷阱(最高频的 JSX 坑):

    // ❌ count 为 0 时会渲染出「0」
    {count && }
    // 渲染结果:
    0
    // 原因:0 是「假值但可渲染」的值 —— React 会把它当作子节点渲染出来 // (而 false / null / undefined / true 会被忽略) // ✅ 四种修复 {count > 0 && } {!!count && } {Boolean(count) && } {count ? : null}
    值 {value && <X />} 的结果
    0 渲染出 0 ⚠️
    ''(空字符串) 渲染出空(不可见,但仍是文本节点)
    NaN 渲染出 NaN ⚠️
    null / undefined / false / true 什么都不渲染 ✅
    [](空数组) 什么都不渲染 ✅

    四、列表渲染与 key

    // 基本写法
    {list.map((item) => (
                // ⭐ key 必须在 map 返回的最外层元素上
    ))}
    
    // ❌ 常见错误:key 加在了内部元素上
    {list.map((item) => (
      
    {/* ⚠️ 警告 + 失效 */}
    ))} // ❌ 用 index 作 key(列表会增删排序时是 bug) {list.map((item, index) => )} // ✅ 稳定唯一 ID {list.map((item) => )}
    // 渲染数组的其它写法
    // ① 先 map 成数组(推荐)
    const items = list.map((x) => 
  • {x.name}
  • ); return
      {items}
    ; // ② 用 Fragment 包多个兄弟元素 {list.map((item) => (
    {item.term}
    {item.desc}
    ))} // ③ 简写形式不能带 key,需要 key 时必须用 {list.map((item) => ( {item.name} ))}

    key 的深入讨论(为什么不能用 index、原理是什么)见 [4.5 key 的作用与选择](../04-渲染机制与原理/4.5-key的作用与选择.md)。

    五、属性(Props)的写法

    // ① 字符串字面量用引号(不写引号则是表达式)
    
    // ② 表达式用花括号 {user.name} // ③ 布尔属性 // 等价于 disabled={true} // 不生效 // ④ 展开传递(把对象的所有属性传下去) const props = { id: 'a', className: 'x', onClick: handleClick };
    // ⑤ 展开后覆盖(后面的生效)
    // className 被覆盖为 override // ⑥ 事件用驼峰、函数引用

    属性名的变化(HTML → JSX):

    HTML JSX 原因
    class className class 是 JS 保留字(历史上)
    for htmlFor for 是 JS 保留字
    tabindex tabIndex 驼峰命名约定
    onclick onClick 同上
    style="color: red" style={{ color: 'red' }} 接受对象而非字符串
    contenteditable contentEditable 驼峰
    aria-* / data-* 保持连字符写法 标准属性
    colspan / rowspan colSpan / rowSpan 驼峰

    六、样式与 className

    // ① 内联 style 是对象(属性名用驼峰)
    
    // ② className 拼接
    // 或用 clsx / classnames 库
    // ③ 条件样式
    // ④ CSS 变量
    // ⚠️ 常见错误
    
    // ❌ 报错:style 需要对象
    // ❌ 应写成 fontSize
    // ⚠️ 警告:应写 className
    // ✅ 值需要带单位(除无单位属性)

    七、语法约束

    // ① 必须有一个根节点(或用 Fragment)
    return (
      
    );
    // Fragment 简写:不会产生额外 DOM
    return (
      <>
        
        
      
    );
    
    // ② 标签必须闭合
    
    // ✅ // ✅ 自闭合
    // ✅
    // ❌ 必须写
    // ③ 组件名必须大写开头(否则被当作 HTML 标签) // ✅ 组件 // ❌ 被当成自定义 HTML 元素,不会渲染组件 // ⚠️ 同上 // ④ 用小写点的形式引用组件(适合组件库导出) import * as Form from './Form'; // ✅ 会被当作组件(有点号) // ⑤ 注释要用花括号包起来
    {/* 这是 JSX 注释 */}
    // ⑥ 属性值不能是未定义变量(会变成 undefined 属性)
    // 属性不会输出(等于没写)

    八、常见坑

    // ① && 遇到 0 会渲染出 0(见上文)
    {count && }
    
    // ② key 加错位置
    {list.map((x) => 
    )} // ⚠️ 应加在最外层 // ③ 用 index 作 key {list.map((x, i) => )} // ⚠️ 列表会变动时出 bug // ④ 直接渲染对象
    {user}
    // ❌ 报错 // ⑤ 在 JSX 里写语句 {list.forEach((x) => console.log(x))} // ⚠️ forEach 返回 undefined,不渲染 {list.map((x) => { })} // ❌ 箭头函数用了 {} 但没 return // ⑥ 箭头函数返回对象要加括号 {list.map((x) => ({ id: x.id }))} // ✅ {list.map((x) => { id: x.id })} // ❌ 被当作代码块 // ⑦ 拼接字符串时忘了空格
    {a}{b}
    // 'ab'(没有空格)
    {a} {b}
    // 'a b' // ⑧ 布尔属性写成字符串

    面试延伸

    1. 「JSX 是什么?编译后变成什么?」

    JSX 是 React.createElement() 的语法糖(React 17+ 是 jsx / jsxs 运行时)。编译后返回一个 React Element 对象(含 type / props / key / ref / $$typeof),它是普通 JS 对象而不是 DOM 节点,也不是字符串。React 在后续渲染阶段才根据它创建/更新真实 DOM。

    1. 「为什么 React Element 上要加 $$typeof?」

    因为它是 Symbol 类型,JSON.parse 无法伪造 Symbol。早期 React 允许服务端返回 JSON 后直接渲染,攻击者可以构造一个 type 为恶意标签或携带危险 props 的「假 Element」注入页面。加上 Symbol 标识后,只渲染「由 React 真正创建」的元素,堵住了这条 XSS 途径。

    1. 「JSX 里为什么不能写 if / for?」

    因为 { } 里要求的是表达式(有返回值),而 if / for 是语句(不产生值)。这也正是 JSX 里大量使用三元表达式和逻辑与的原因。需要多分支逻辑时,通常把计算提到 JSX 外部(用变量或提前 return),或用映射表的方式。

    1. 「{count && <List />} 有什么问题?」

    当 count 为 0 时,0 && ... 的结果是 0,而 React 会把数字 0 渲染成文本(0 是「假值但可渲染」的值),于是页面上出现一个 0。同理 NaN 也有这个问题(会被渲染成 NaN)。修复方式是改用 count > 0 && ...、!!count && ... 或三元表达式 —— 即保证左侧是布尔值。

    1. 「哪些值会被 React 忽略、哪些会被渲染?」

    会被忽略(什么都不渲染):null、undefined、false、true、空数组。 会被渲染:字符串、数字(包括 0 和 NaN)、元素、数组(逐项渲染)、Fragment。 会报错:普通对象(Objects are not valid as a React child)、函数。 这个差异正是「&& 的 0 陷阱」的根源。

    1. 「React 17 的自动运行时(Automatic Runtime)改了什么?」

    ① 不再需要 import React from 'react'——编译器自动从 react/jsx-runtime 注入 jsx / jsxs 函数(这也让未使用的 React 变量不再出现在产物中);② 生成的是 jsx() 调用而不是 React.createElement(),参数结构不同(children 放进 props);③ jsx 与 jsxs 分开处理静态与动态子节点,略微提升性能并改善 key 的检查。这是 React 17 引入、React 18/19 默认使用的模式。

    1. 「<Comp /> 和 Comp() 有什么区别?」

    <Comp /> 是创建元素并交给 React 渲染(React 会把它作为独立的 Fiber 节点处理,Hooks 归属正确、有自己的状态与生命周期);Comp() 是直接把函数当作普通函数调用,返回值被内联到父组件的渲染结果中——它的 Hooks 会被挂到父组件身上,导致状态混乱、调用顺序错乱。所以永远用 <Comp /> 而不是 Comp()。

    1. 「JSX 里的注释怎么写?为什么 { /* */ }?」

    因为 JSX 的 { } 里是 JS 表达式上下文,所以注释必须写成 {/* ... */} 的形式(外层花括号进入表达式上下文,里层是 JS 注释)。在 JSX 元素外部(如函数体里)可以直接写 // 或 /* */。

    一句话速记

    JSX 是 createElement 的语法糖,产物是不可变的 React Element 对象(用 Symbol 防伪造);{ } 里只能写表达式,所以要靠三元与 && 做条件渲染;{count && <X/>} 在 count === 0 时会渲染出 0(左侧要保证是布尔);列表用稳定唯一 key 并加在 map 返回的最外层元素上;样式是对象、类名是 className;React 17+ 不需要 import React,因为编译器会自动注入 jsx 运行时。

    赞(0) 打赏
    未经允许不得转载:陌上寒 » 1.1 JSX 语法与本质

    评论 抢沙发

    © 2017-2026   陌上寒   网站地图 京ICP备16036924号

    请求次数:118 次,加载用时:1.210 秒,内存占用:74.98 MB

    觉得文章有用就打赏一下文章作者

    非常感谢你的打赏,我们将继续给力更多优质内容,让我们一起创建更加美好的网络世界!

    微信扫一扫

    支付宝扫一扫