ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JSX 编译原理与实战:从语法糖到 React/Vue3 应用

JSX 编译原理与实战:从语法糖到 React/Vue3 应用 1. 从一个被问烂了的问题说起JSX 到底是什么如果你在团队里带过新人或者混过任何前端社群一定见过这个场景有人贴出一段 React 代码里面混着 HTML 标签和 JavaScript 逻辑然后问——“这玩意儿到底是 HTML 还是 JS浏览器怎么能认识这种东西”这个问题问得很好因为它触及了 JSX 的本质。我第一次接触 JSX 是在 2016 年前后当时从 Vue 的模板语法切过来看到render函数里直接写标签第一反应是“这不是倒退吗逻辑和视图又混在一起了”。但用了一周之后我改观了。JSX 不是倒退它是一种把视图结构表达为 JavaScript 值的方式只不过借用了类似 HTML 的语法糖。先把结论放在这里JSX 是 JavaScript 的语法扩展它允许你在 JS 代码里写类似 HTML 的结构但它最终会被编译成普通的 JavaScript 函数调用。浏览器本身不认识 JSX认识它的是 Babel、TypeScript 编译器、SWC 这类工具。你写的divhello/div经过编译后变成React.createElement(div, null, hello)或者在新版运行时下变成更精简的jsx(div, { children: hello })。这个转换过程是理解 JSX 的关键。很多人用了两三年 React依然说不清楚 JSX 和createElement的关系遇到“React 未定义”的报错就懵了。其实只要把编译前后的对应关系搞清楚大部分 JSX 相关的疑惑都会迎刃而解。这篇文章我打算从实际使用的角度出发把 JSX 的编译原理、语法细节、和 Vue 模板的对比、常见报错排查、以及在新版本 React 和 Vue3 里的用法都过一遍。适合已经写过一些 React 或 Vue、但对 JSX 底层机制还不太清楚的同学也适合正在做技术选型、想搞清楚“为什么 React 要用 JSX”的开发者。2. JSX 的编译原理你写的标签到底变成了什么2.1 从标签到函数调用一次完整的编译拆解先看一段最基础的 JSXconst element h1 classNametitleHello JSX/h1;这段代码在 Babel 的经典转换模式下会变成const element React.createElement( h1, { className: title }, Hello JSX );React.createElement接收三个参数类型、属性对象、子元素。子元素可以是多个所以从第三个参数开始都是 children。如果 children 是动态表达式比如{name}它就直接作为参数传进去。到了 React 17 之后官方引入了新的 JSX 转换New JSX Transform编译结果变成了import { jsx as _jsx } from react/jsx-runtime; const element _jsx(h1, { className: title, children: Hello JSX });这个变化的意义在于你不再需要在每个文件顶部手动import React from react。以前如果忘了引入 React就会报React must be in scope when using JSX。新转换自动从react/jsx-runtime引入所需的函数减少了样板代码也让 tree-shaking 更高效。那createElement返回的到底是什么它返回的是一个普通的 JavaScript 对象通常被称为React Element结构大致如下{ type: h1, props: { className: title, children: Hello JSX }, key: null, ref: null, _owner: null }注意这个对象不是 DOM 节点也不是组件实例它只是一个描述“我想要什么样的 UI”的普通对象。React 在渲染阶段会读取这个对象再决定如何创建和更新真实的 DOM。这就是为什么说 React 的渲染是“声明式”的——你描述结果React 负责过程。2.2 为什么不用模板语法而要用 JSX这个问题在技术选型时经常被拿出来讨论。Vue 用的是模板语法Angular 用的是模板加指令React 偏偏选了 JSX。背后的考量其实很实际。模板语法的优势是约束性强视图和逻辑天然分离设计师或者后端同学也能看懂。但它的问题在于一旦你要在模板里做复杂的条件判断、循环嵌套、动态组件切换就需要发明各种指令v-if、v-for、v-slot、v-bind、v-on……指令越堆越多学习成本反而上去了。JSX 的思路不一样。它不发明新语法而是复用 JavaScript 本身的表达能力。条件判断用或三元表达式循环用map组件复用直接就是函数调用。你不需要记额外的指令只要会 JS就能表达任意复杂的视图逻辑。我个人的体会是简单页面用模板更清爽复杂交互用 JSX 更灵活。这也是为什么 Vue3 在保留模板的同时也完整支持了 JSX——它知道有一部分场景模板确实不如 JSX 顺手。2.3 JSX 不是 HTML几个必须记住的差异很多人把 JSX 当 HTML 写然后踩一堆坑。这里列几个高频差异点HTML 写法JSX 写法说明classboxclassNameboxclass 是 JS 保留字fornamehtmlFornamefor 是 JS 保留字onclickfn()onClick{fn}事件用驼峰传函数引用stylecolor:redstyle{{color:red}}style 接收对象inputinput /所有标签必须闭合!-- 注释 --{/* 注释 */}注释要包在表达式里这些差异不是 React 故意为难人而是因为 JSX 最终要变成 JavaScript 对象属性名必须符合 JS 的标识符规则。class和for在 JS 里是关键字所以只能改名。事件处理用驼峰是因为 React 自己实现了一套合成事件系统属性名需要统一规范。注意在 JSX 里写style时属性名也要用驼峰比如backgroundColor而不是background-colorfontSize而不是font-size。这一点新手特别容易忘。3. JSX 的核心语法与实操要点3.1 表达式嵌入花括号里能放什么JSX 里用一对花括号{}来嵌入 JavaScript 表达式。注意关键词是“表达式”不是“语句”。这意味着能放的变量{name}函数调用{formatDate(time)}三元运算{isLogin ? Home / : Login /}逻辑与{list.length 0 List /}数组映射{items.map(item Item key{item.id} /)}对象属性访问{user.profile.avatar}不能放的if语句{if (x) { ... }}是语法错误for循环同样不行变量声明{const a 1}不行这个限制的原因是 JSX 的花括号位置本质上是一个函数参数的位置你只能放一个“值”不能放一段“控制流”。需要条件分支就用三元或需要循环就用map。我见过有人为了在 JSX 里写if硬生生搞出 IIFE立即执行函数虽然能跑但可读性很差。更好的做法是把逻辑抽到渲染函数外面或者拆成独立组件。3.2 列表渲染与 key 的真正作用列表渲染是 JSX 里最容易出问题的地方核心就在key上。{users.map(user ( UserCard key{user.id} user{user} / ))}key是给 React 的 diff 算法用的。当列表更新时React 需要判断哪些元素是新增的、哪些是移动的、哪些是删除的。如果没有 keyReact 只能按索引顺序对比一旦列表顺序变化就会导致错误的复用。举个实际例子一个待办列表每项带一个输入框。如果你用数组索引当 key删除第一项后第二项的输入框内容会“串”到第一项上。因为 React 认为索引 0 还是索引 0只是 props 变了于是复用了同一个 DOM 节点输入框里的用户输入就被保留了。所以 key 的选择原则是稳定、唯一、可预测。数据库 id 是最好的选择其次是业务上唯一的字段。实在没有才退而求其次用索引但要确保列表不会重排序或增删。实操心得如果列表项内部有表单输入、动画状态、或者组件内部 state用索引当 key 几乎一定会出 bug。这种问题排查起来很隐蔽因为界面看起来“差不多对”但交互行为就是不对劲。3.3 条件渲染的几种写法与取舍条件渲染在 JSX 里有好几种写法各有适用场景// 三元适合二选一 {isLoading ? Spinner / : Content /} // 逻辑与适合“有则显示” {error ErrorTip message{error} /} // 提前返回适合整个组件级别的分支 if (!user) return Login /; return Profile user{user} /; // 变量提取适合复杂条件 let content; if (status loading) content Spinner /; else if (status error) content Error /; else content Data /;这里有个坑要特别提醒用做条件渲染时如果左边的值是0或空字符串会直接把0渲染到页面上。因为0 div的结果是0而 React 会渲染数字 0。// 危险list.length 为 0 时会显示 0 {list.length List /} // 安全转成布尔值 {list.length 0 List /}这个 bug 我见过太多次了尤其是列表数量从有到无的时候页面上突然冒出一个孤零零的 0排查半天才发现是这里。3.4 属性传递展开运算符与默认值JSX 支持用展开运算符批量传递 propsconst props { name: Tom, age: 18, role: admin }; User {...props} /这在封装高阶组件或者透传属性时非常方便。但要注意顺序问题User {...props} nameJerry /后面的name会覆盖展开对象里的name。这个特性可以用来设置默认值或者强制覆盖某些属性。另一个常见需求是给 props 设默认值。函数组件可以用参数默认值function Button({ type default, size medium, children }) { return button className{btn-${type} btn-${size}}{children}/button; }如果项目里用了 TypeScript还可以配合defaultProps或者直接类型标注。不过 React 官方已经不建议在新代码里用defaultProps了函数组件直接用 ES6 默认参数更清晰。4. JSX 在 React 与 Vue3 中的实战差异4.1 React 中的 JSX一切皆表达式在 React 里JSX 是官方推荐的写法整个生态都围绕它构建。组件就是函数返回 JSX逻辑就是 JS写在函数体里。这种统一性带来的好处是心智负担小——你不需要在“模板世界”和“逻辑世界”之间来回切换。React 的 JSX 有几个进阶用法值得掌握Fragment当你想返回多个并列元素又不想多加一层 DOM 时function List() { return ( liItem 1/li liItem 2/li / ); }.../是React.Fragment的语法糖编译后不会产生额外节点。动态组件根据变量决定渲染哪个组件const components { home: Home, about: About, contact: Contact }; const Current components[route]; return Current /;注意这里变量名必须大写开头因为 JSX 编译时通过首字母大小写来区分“HTML 标签”和“组件引用”。小写会被当成字符串标签大写才会被当成变量。render props 与 children 作为函数DataProvider {(data) Chart data{data} /} /DataProvider这种模式在 React 生态里很常见本质上是把 JSX 的 children 当成普通 prop 传递。4.2 Vue3 中的 JSX从模板到灵活表达Vue3 对 JSX 的支持比 Vue2 完善了很多。在 Vue2 时代JSX 需要额外的 Babel 插件而且和模板的语法差异较大。Vue3 的vue/babel-plugin-jsx让 JSX 写起来更接近 React 的体验同时保留了 Vue 的响应式特性。在 Vue3 里写 JSX几个关键差异点// 事件绑定用 on 前缀 button onClick{handleClick}点击/button // v-model 用 modelValue onUpdate:modelValue Input modelValue{value} onUpdate:modelValue{v value v} / // 插槽用 v-slots MyComponent v-slots{{ header: () h1标题/h1, default: () p内容/p }} /Vue3 的 JSX 里ref的用法也有区别。模板里用refmyRefJSX 里需要用ref{myRef}或者ref{(el) myRef.value el}。我实际用下来Vue3 的 JSX 适合这几类场景需要高度动态的组件结构、需要把视图逻辑封装成可复用的函数、或者团队本身有 React 背景。但如果只是普通页面模板的编译优化比如静态提升、补丁标记还是更有优势。4.3 两者对比选型时该考虑什么维度React JSXVue3 模板Vue3 JSX学习曲线需要理解 JS 表达式指令语法直观需要 JS 基础灵活性极高中等高编译优化运行时 diff编译时优化强接近 React类型支持TS 友好需要 volarTS 友好生态工具丰富丰富相对少选型没有绝对答案。我的建议是如果团队 JS 功底扎实、交互复杂度高React JSX 或 Vue3 JSX 都合适如果团队人员背景多样、页面以展示为主Vue 模板的约束性反而是优势。5. 常见报错与排查技巧实录5.1 高频报错速查表报错信息原因解决方法React must be in scope旧版 JSX 转换未引入 React引入 React 或升级到新转换Adjacent JSX elements must be wrapped返回了多个并列元素用 Fragment 或父元素包裹Objects are not valid as a React child直接渲染了对象渲染对象的某个属性Each child in a list should have a unique key列表缺少 key添加稳定唯一的 keyCannot read property xxx of undefined访问了未初始化的数据可选链或条件渲染Invalid hook callHook 在组件外调用检查调用位置和 React 版本5.2 几个真实踩坑记录坑一JSX 里的注释写法刚写 React 时我在 JSX 里用//写注释结果注释内容被渲染到页面上了。正确写法是{/* 注释 */}或者写在 JSX 外面的 JS 区域。坑二className 拼写错误有次排查一个样式不生效的问题查了半小时 CSS最后发现是写成了class而不是className。React 不会报错只是默默忽略了这个属性。这种问题在 JSX 里很常见因为编译器不会校验属性名是否合法。坑三事件绑定传参// 错误立即执行了 button onClick{handleClick(id)}点击/button // 正确传函数引用 button onClick{() handleClick(id)}点击/button第一种写法在渲染时就会执行handleClick而不是点击时。这个错误在循环里特别容易犯。坑四布尔属性// disabled 为 false 时属性不会出现在 DOM 上 input disabled{false} / // 但如果写成字符串就会一直生效 input disabledfalse /在 JSX 里布尔属性传false会直接省略该属性传字符串false反而会被当成真值。这个和 HTML 的行为不一样需要特别注意。5.3 调试 JSX 的实用技巧用 Babel 在线工具看编译结果遇到不确定的 JSX 写法可以贴到 Babel REPL 里看编译后的代码一目了然。React DevTools 看组件树JSX 写出来的组件结构在 DevTools 里会以树形展示可以快速定位是哪个组件渲染出了问题。在 JSX 里打日志虽然不优雅但在return之前console.log一下变量能快速确认数据是否符合预期。更好的做法是用断点调试。TypeScript 加持给 props 加类型定义后大部分属性名拼写错误、类型不匹配的问题在编辑器里就能发现不用等到运行时。6. 关于 JSX 的一些个人体会写了这么多年 JSX我最大的感受是它把“视图”从一种特殊的模板语言拉回到了普通编程语言的范畴。这意味着你不需要为视图单独学一套语法也不需要记住各种指令的边界情况。你用的还是函数、变量、条件、循环只是多了一层类似 HTML 的语法糖。当然JSX 也不是没有代价。它让视图和逻辑的边界变得模糊如果团队没有约定很容易写出一个几百行的组件函数里面混杂着数据请求、状态计算、事件处理和视图渲染。我的做法是单个组件的 JSX 部分尽量控制在 50 行以内超了就拆子组件或者抽渲染函数。另外新版的 JSX 转换确实省心不少不用再手动引入 React 了。但如果你的项目还在用旧版本构建工具升级时要注意 Babel 配置和 React 版本的对应关系不然会出现运行时找不到jsx函数的报错。最后分享一个小技巧如果你在 Vue3 项目里用 JSX记得在vite.config.js里配置vitejs/plugin-vue-jsx并且在tsconfig.json里设置jsx: preserve。这两个配置缺一不可否则要么编译报错要么类型提示失效。这个组合我试过好几次实测下来很稳。
返回列表