拓冰建站拓冰建站
首页 / 资讯中心 / 正文

JSX语法规则详解与实操练习:从原理到工程应用

“005-006 jsx语法规则、jsx小练习”——看到这个编号基本就能猜到这是一套前端入门课程里的某个节点。我自己带新人时一般在第5、第6次课响应讲JSX前面刚讲完React.createElement的基础后面马上要进入组件开发这个位置很关键学得好后续写组件就像写HTML一样顺手学得不好后面到处都是“花括号包裹”“驼峰命名”这类小坑。这篇文章我会把课程里讲的JSX语法规则重新梳理一轮再带上4个我实际给新人用的小练习每个练习都给出能直接跑起来的代码和我在旁标注的“为什么这么写”。JSX这套东西资料很多但多数讲得太散要么只讲规则不给场景要么狂堆代码不解释背后编译逻辑。我尽量用带项目的口吻讲清楚JSX不是模板引擎它是JavaScript的语法糖理解了这一层规则就不需要死记。1. 内容整体设计与思路拆解1.1 先搞清楚JSX到底是个什么东西我第一次接触JSX时最大的困惑是这不就是HTML吗为什么还非得起个新名字后来看编译产物才反应过来JSX既不是HTML也不是模板字符串它是React.createElement这个调用的语法糖。举个最简单的例子。你写const element h1 classNametitle你好/h1;Babel会把这段转译成const element React.createElement(h1, { className: title }, 你好);注意看标签名变成了第一个参数属性集合变成了第二个参数子节点变成了第三个及之后的参数。也就是说JSX树在编译阶段已经被拍扁成一棵JavaScript对象树了而不是等到运行时才去解析标签字符串。理解了这一点很多规则就说得通了。比如“标签必须闭合”——因为createElement需要完整描述一个节点树一个没闭合的标签会造成结构歧义编译器没法确定子节点的边界。再比如“只能有一个根节点”——因为函数只能返回一个值如果你写了并列的两个根编译出来的createElement不知道该把哪个作为返回值。这个认知是整篇文章的基石。后面所有规则我都会尽量往这条主线上去靠而不是孤立地罗列知识点。1.2 为什么要设计成嵌入在JavaScript里的结构早几年前端圈争论过一个话题模板应该在HTML标签里写逻辑还是应该在JavaScript里写模板React选的是后者Vue最初选了前者到了Vue3也开始提供JSX或h函数的能力。这里面的逻辑值得多想一步。JSX最实在的优势是“组件一切皆JS”。模板语言里你需要学习v-if、v-for、ngIf这类指令但在JSX里条件判断直接写if或三元表达式列表循环直接调用map复用逻辑就是定义一个函数变量。学JSX你不需要再学一套指令系统只需要把JavaScript基础打好。另一个容易被忽略的点是类型检查。既然JSX是JavaScript语法的一部分TypeScript就可以对JSX里的属性名、事件参数做静态类型校验。比如说button onClick{handleClick}如果handleClick函数签名不匹配编辑器里就会直接标红。这一点在大型团队协作里价值极大——模板引擎时代这类问题只能在运行时暴露。当然JSX也不是没有代价。它要求构建工具链支持编译不能直接扔到浏览器里跑。但今天Vite、Next.js这类脚手架已经把这层复杂度封装好了代价对这个时代来说实际可以忽略。1.3 从模板语法切换到JSX的思维转变我接触过不少从Vue转React或者同时用两者的同事他们最不适应的点往往不是JSX本身而是思路上的切换。Vue模板是这么写条件渲染的div v-ifvisible p显示这段文字/p /divJSX里则是div { visible p显示这段文字/p } /div前者读起来像“对这段HTML做一个指令”后者读起来像“在JavaScript表达式里嵌入一段结构”。同样的事前者偏向声明式指令后者偏向计算式表达式。说不上谁绝对更好但你必须清楚自己当前在用什么范式。一旦建立了“JSX是JavaScript表达式”这个认知写列表渲染时会下意识想到map写复杂逻辑时会想到先算出一个变量再放进{}而不是到处找指令。这条思维转换是我在教学设计里排在语法规则之前的。2. 核心语法规则详解一条一条说清楚2.1 根节点为什么只能有一个最外层标签JSX表达式必须有一个根级容器否则编译会直接报错Adjacent JSX elements must be wrapped in an enclosing tag。原因是前面说的一个createElement调用只能生成一个节点。你写了两个并列的div编译后就成了两个函数调用结果函数却没规定怎么把它们拼起来。解决办法有三种包一层普通div用React Fragment缩写.../返回一个数组不推荐因为需要加key实际开发中我推荐优先用Fragment。多包一层div会无端增加DOM层级很多样式问题就是这么来的。Fragment不会在DOM树上新增任何节点。function CardList() { return ( Card title一 / Card title二 / / ); }顺便说一个细节/不能设置key如果是动态循环场景需要用完整的Fragment key{item.id}。这个坑很容易被忽略。2.2 标签必须闭合自闭合标签不能省JSX沿用了XML的严格闭合规则div必须有/divimg必须写成img /否则编译会失败。很多人在HTML里习惯不写img的闭合斜杠搬到JSX里就容易报错。这里给一个排查建议如果你复制了一段现成的HTML片段到JSX里第一件事就是检查img、input、br、hr这几个自闭标签有没有补上斜杠。还有一个大小写问题JSX里小写标签名会被当成HTML内置元素大写开头的会被当成组件。所以如果你自定义了一个组件但忘了大写比如写了userCard编译会把它当作usercard这个HTML标签去处理运行时告诉你这个元素不认识。同样把div写成DivReact也会尝试把它当成组件渲染然后失败。2.3 花括号里只能放表达式放不了语句这是JSX最容易踩出问题的地方。{}里可以放变量、函数调用、三元表达式、数组map的结果但不能放if、for、switch、变量声明。我见过不少新手的尝试{ if (condition) { return p对/p } }直接编译报错。原因还是那句话{}最终会作为参数传给createElement(..., children)参数位置只能是一个表达式不能是语句。遇到这种情况常规解法是在JSX外面先把逻辑算好let content; if (condition) { content p对/p; } else { content p错/p; } return div{content}/div;或者直接用三元表达式和短路运算。要建立一个习惯JSX内部尽量只做简单判断复杂逻辑往函数体里挪这样代码可读性反而更好。2.4 class要写成className内联样式是对象这是初学JSX时出现频次最高的小错误。在JSX里写div classbox虽然浏览器能渲染出来控制台却会有一个warning而且React官方明确不推荐这么做。原因很直接class是JavaScript的保留字你在JavaScript的表达式上下文里写class解析器会感到困惑。所以JSX把HTML的class属性改名为className。内联样式就更有意思了它要求传一个对象且属性名是驼峰div style{{ backgroundColor: #f0f0f0, fontSize: 14px, padding: 8px }} 内容 /div注意这里有两层花括号外层是JSX表达式语法“我要进入JavaScript空间”内层是对象字面量。很多新人问我为什么CSS属性要写成驼峰——因为CSS属性名里的连字符在JavaScript对象里不好写所以React把它们统一处理成了驼峰风格。实际开发时动态样式经常配三元表达式div style{{ color: isActive ? #fff : #333 }} 状态文字 /div要注意到style对象里的数值属性如width: 100React会默认加上px单位。但像opacity、lineHeight这类不会加单位这个规则不必硬记遇到特殊场景查一下文档就行。2.5 属性值静态字符串和动态表达式JSX属性的写法分两种场景静态场景直接写字符串img src/logo.png alt站点Logo /动态场景用花括号包表达式img src{logoUrl} alt{logoAltText} /还有一个容易忽略的细节布尔值属性。HTML里有disabled、readonly这类属性只要标签上出现就生效。在JSX里要遵守“传值就要显式传”的风格button disabled{true}不可用/button button disabled{false}可用/button如果你漏写属性值比如button disabledJSX会把它默认当成{true}。这个行为在组件传参时有时会引发意想不到的Bug所以要养成显式写布尔值的习惯。2.6 列表渲染map方法代替v-forJSX里渲染列表的思路和Vue模板截然不同。Vue模板用v-for指令JSX直接用JavaScript原生数组方法const users [ { id: 1, name: 张三 }, { id: 2, name: 李四 } ]; function UserList() { return ( ul {users.map(user ( li key{user.id}{user.name}/li ))} /ul ); }map返回的是一个JSX元素数组React会把数组依次渲染到对应位置。这再次印证了那句“JSX是JavaScript表达式”——数组是表达式map返回数组所以一切顺理成章。key属性是列表渲染必须关心的点。它给React的Diff算法提供身份标识帮助框架判断哪些元素变化了、哪些可以复用。我见过有人用数组下标当key短期内没出问题但一旦列表发生了插入、删除、排序就会出现状态错乱、渲染位置不对这类诡异Bug。正确做法是使用业务里真正唯一的ID。如果没有现成ID可以生成一次稳定ID再存下来而不是每次渲染都Math.random()。2.7 条件渲染短路与三元的细节JSX的条件渲染一般有三种写法按场景选择二元短路{visible p显示/p}三元表达式{visible ? p显示/p : p隐藏/p}提前returnif (!visible) return null;用时要特别小心一个经典坑当左边是数字0时React会渲染出0这个字符。比如{count p有数据/p}如果count是0页面会莫名其妙显示一个“0”。原因很简单0 表达式的结果是0而React认为0是有效渲染内容。解决办法是把判断写成布尔值{count 0 p有数据/p}提前return通常用于权限控制、空状态拦截这类较复杂的逻辑。它不算JSX规则而是把控制流放在函数体里JSX内部保持干净我建议在业务代码里优先用这种。2.8 注释与转义JSX区域的边界问题在JSX里写注释也有自己的一套约定。写在子节点的注释需要包在花括号里div {/* 这是一个注释 */} p文字内容/p /div如果写在属性附近那你实际上仍在JSX表达式区块内按JavaScript注释来写const el /* 注释 */ div内容/div;很多新人想在标签内部直接写!-- 注释 --这在JSX里是行不通的。它会原样输出成HTML注释虽然浏览器不报错但不符合React组件的精神。转义方面JSX默认会对文本内容里的HTML实体和标签进行转义处理防止注入攻击。你写{p}页面会显示字面的p而不是创建一个p标签。这是React的安全机制不要为了“方便”用dangerouslySetInnerHTML去绕过它除非你对传入内容有完全的信任。2.9 核心规则速查表为了方便备查我把上面讲到的规则整理成一张表规则类别正确写法错误写法原因根节点用.../包裹并列多个根元素单个表达式只能返回一个节点标签闭合img /、input /img编译需要确定节点边界组件命名UserCarduserCard小写被视为HTML标签CSS类名classNameclassclass是JS保留字内联样式style{{ color: red }}stylecolor: red样式是JavaScript对象条件渲染{count 0 px/p}{count px/p}避免渲染数字0列表key用业务唯一ID数组下标帮助Diff识别节点注释{/* 注释 */}!-- 注释 --处于JS表达式区域这张表是我做团队内训时整理出来的前端基础参差不齐的情况下大家照着表过一遍JSX踩坑率明显下降。3. JSX小练习逐题拆解照着做就能上手3.1 练习一在JSX里渲染变量与简单表达式目标理解{}不是装饰它代表进入JavaScript运行空间。新建一个组件定义几个基础变量然后把它们按不同方式渲染出来。function BasicRender() { const userName 小明; const age 18; const tags [前端, React, 篮球]; return ( div h1用户信息/h1 p姓名{userName}/p p年龄{age 18 ? 成年 : 未成年}/p p标签数{tags.length} 个/p p 标签 {tags.join(、)} /p /div ); }这个练习的核心价值在于体会“表达式”的边界。userName是变量age 18 ? ... : ...是三元表达式tags.length是属性访问tags.join(...)是方法调用。它们在{}里都合法因为它们都会立刻产生一个值。你可以试着在{}里放一句const i 1;页面马上会报语法错误。看见报错不要慌这恰好能帮你建立“哪些能放、哪些不能放”的直觉。运行这个组件页面上会正确显示用户信息。如果出现“对象不能作为React子元素”的报错多半是你在{}里直接塞了一个普通对象比如{ { name: 张三 } }。React允许函数、数组、字符串、数字作为子节点但普通对象不行。3.2 练习二根据登录状态展示不同内容目标掌握条件渲染的两种主流写法并理解它们在可读性上的差异。定义一个isLoggedIn状态根据它切换欢迎语和登录按钮。function LoginStatus() { const isLoggedIn true; return ( div {isLoggedIn ? ( p欢迎回来用户/p ) : ( button请先登录/button )} {isLoggedIn span当前在线/span} /div ); }三元表达式适合“二选一”的场景——要么显示A要么显示B。适合“单条件展示”的场景——满足条件就显示不满足就什么都不显示。我给出的代码里两个都用到了这样你能直观对比。很多人觉得比三元“简洁”但简洁的前提是语义清晰。如果你的条件里包含数字运算务必先处理布尔转换比如{isLoggedIn true ...}。这个练习还有一个扩展建议把isLoggedIn改成可以从props传入这样组件就变成受控的跟实际业务接轨。3.3 练习三渲染待办列表并高亮已完成项目标掌握map列表渲染与key的正确用法同时练习通过动态className和style控制视觉状态。准备一份待办数据用map渲染成列表已完成项加删除线标记。const todos [ { id: t-001, text: 学习JSX语法, done: true }, { id: t-002, text: 完成小练习, done: false }, { id: t-003, text: 整理笔记, done: false } ]; function TodoList() { return ( ul {todos.map(todo ( li key{todo.id} style{{ textDecoration: todo.done ? line-through : none, color: todo.done ? #999 : #333 }} {todo.text} {todo.done 已完成} /li ))} /ul ); }key务必放在map直接产生的那个元素上。这里key{todo.id}放在li上是正确的。如果放到了li内部的某个子元素上React依然会警告而且Diff优化完全失效。另一个细节同样一条列表项textDecoration和color都根据done动态变化。这是实际项目里最常见的动态样式写法。你也可以改用三元拼classNameli className{todo.done ? todo-item done : todo-item}两种写法各有适用场景。style适合动态性极强的内联视觉属性className适合样式较多、需要配合CSS文件的场景。我个人的习惯是超过两个动态属性时优先考虑className降低JSX里的噪音。试着往todos里插入一条新数据观察key稳定时的渲染表现。如果换成下标key{index}在插入第一条时视觉可能看不出区别但如果你加入了复选框交互就会看到状态错乱。3.4 练习四事件绑定与受控输入目标理解JSX事件系统的写法并组装一个小而完整的交互组件。做一个简单的输入框点击按钮把内容追加到列表。import { useState } from react; function AddItemBox() { const [inputValue, setInputValue] useState(); const [items, setItems] useState([默认项目]); const handleSubmit () { if (inputValue.trim() ) return; setItems([...items, inputValue]); setInputValue(); }; return ( div input value{inputValue} onChange{event setInputValue(event.target.value)} placeholder输入新项目 / button onClick{handleSubmit}添加/button ul {items.map((item, index) ( li key{item - index}{item}/li ))} /ul /div ); }这个练习涉及内容超出了JSX语法本身包含状态管理和事件绑定。但它恰好是JSX最常用到的完整场景所以我把它放在这里当作综合练习。事件绑定的写法要注意onClick{handleSubmit}传的是函数引用而不是调用结果handleSubmit()。如果写成后者函数会在渲染执行时立刻执行一次按钮点击反而没反应。这是JSX事件处理高频Bug。受控输入是指输入框的value和onChange合在一起让React完全接管输入值。很多初学者困惑“为什么我在框里打字没反应”多半是写了value{inputValue}却漏了onChange处理。注意这个练习我在key上借用了item - index严格来说这不算稳定ID在插入和排序场景会有问题。练习里它是可接受的但到了真实业务请务必用唯一ID。我会更推荐把练习数据改成带ID的对象数组这样更贴近实际。4. 常见问题与排查技巧实录4.1 报错信息逐个拆从翻译到解法JSX报错通常比较直接但新手容易对着英文报错蒙圈。我挑几个高频的记录下来。Adjacent JSX elements must be wrapped in an enclosing tag并列的JSX元素没有被包在同一个根节点里。检查return里是否有两个根级div用Fragment或外层容器包起来。Unexpected token通常是某处花括号、括号不配对或是在{}里写了语句比如if、for。先看代码里JSX区块和JavaScript区块的边界是不是乱了。div is not defined/yourComponent is not defined见到这个报错第一反应是组件名拼写或导入问题。再确认一下是否以大写字母开头是否从正确路径导入。Each child in a list should have a unique key prop列表渲染没有设置key或key重复。找出map返回的JSX元素给它补一个稳定唯一标识。Objects are not valid as a React child在{}里直接放了普通对象需要用数组、字符串、数字或JSX元素替换。Cannot read properties of undefined (reading map)多半是数据还没加载完成就尝试map此时数组是undefined。解法是在map前加一层空值兜底比如(data || []).map(...)。4.2 编译链观察法把Babel当作调试工具遇到拿不准的JSX语法时与其猜测不如直接看编译结果。浏览器打开 Babel官网的在线编译工具 在左侧输入JSX代码右侧就会实时显示编译后的JavaScript函数调用。这个方法最适合验证前面讲到的“JSX本质是createElement调用”这句话。举个例子div classNamebox onClick{handleClick} 内容 {name} /div编译后你会在右侧看到React.createElement(div, { className: box, onClick: handleClick }, 内容 , name)。这样一来每个属性如何映射、子节点如何排列就一目了然了。遇到奇怪的报错我按照这个思路多跑几遍通常很快能定位。前端工程里最终负责这个编译转化的是各构建工具内置的babel/preset-react或者Vite与SWC的相应插件。理解了产物形态你在排查“为什么页面不支持JSX”时会心里有底。4.3 排错三板斧JSX运行不正常的通用排查思路分享一套我在实际排查中反复用到的固定流程。先看编译期提示。终端或浏览器控制台出现的语法错误优先处理。这类提示通常精确到行和列定位能力最强。再看控制台运行时警告。比如key警告、class名称警告。它们不影响渲染但是隐患应该逐一处理而不是忽略。我见过太多团队Warning堆成山最后真正出Bug时根本找不到有效线索。最后看渲染结果。如果页面渲染出来但不符合预期就在JSX表达式内部插入console.log确认数据形态对不对。重点检查数组是否为空、对象属性名是否拼错、truam运算结果是否符合预期。排查时不要东一榔头西一棒槌。我曾经遇到一个同事排查半小时结果是className写成了class控制台其实早就告警了。所以顺序很重要先看提示再打日志最后上编辑器调试能省大量无效时间。4.4 一个被低估的语法陷阱JSX里的空行与分号很多新人写JSX时喜欢在标签之间敲很多空行以为可以随便排版。实际上JSX的空格处理有自己的规则。在JSX里写在标签之间的换行和缩进默认会被忽略但文本与标签紧挨着时空格可能不被合并。比如div 前 span中/span 后 /div这里“前”和“后”之间实际渲染出的效果可能和你预期的不太一样。解决办法是显式拼接{前 }{后 }或者使用CSS的white-space控制。还有一个现象JSX里你写了分号也经常会被忽略因为编译后它本来就在JavaScript环境里分号有无不影响逻辑。不过为了团队代码风格统一建议遵循项目里的lint规则不要混用。4.5 团队实战里最容易出现的三个JSX代码坏味道最后补充几个评审代码时经常遇到的坏味道纯属经验之谈。第一个是过长的JSX树。一个render里塞了七八层嵌套结构读起来极其痛苦。解决办法是拆组件或拆局部变量让JSX树尽量扁平。推荐把某块子结构抽成一个变量再放进{}const header ( div classNamecard-header h3{title}/h3 span{subtitle}/span /div ); return ( div classNamecard{header}/div );第二个是滥用短路前面提过的数字0问题就是典型。代码评审时我会要求统一用布尔比较或者显式转成布尔值。第三个是内联样式堆成山。JSX里写style对象本身没错但如果style对象里有十几行属性就该考虑改成CSS类或者抽一个样式变量。否则组件逻辑和视觉混在一起维护成本呈指数上升。最后再分享一个我压箱底的习惯每当遇到JSX相关的报错或诡异行为我会先默念一遍“花括号里就是另一个世界标签里是静态结构”。这句话从我带新人到现在一直有效。JSX说穿了就是“JavaScript为主、标签为辅”的组合你越早接受它是代码而不是HTML越不容易被那些看起来很像、其实不兼容HTML的写法绊住。这几个小练习如果你能动手跑一遍再顺手把文末检查清单用在手头项目里JSX语法这块基本就稳了。后续写组件、写Hook、上TypeScript会发现很多所谓的“新知识”其实都是从这棵名为JSX的树上长出来的枝叶。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门