☰
JSX与TSX核心原理详解:从React到Vue 3的完整实践指南
2026/10/3 10:04:04 网站建设 项目流程

这两年前端圈有一个很有意思的现象:很多人是从 Vue 的模板语法开始写页面,后来遇到 React 项目,看到className、onClick、花括号里塞 JS 表达式,第一反应是“这什么玩意,HTML 和 JS 混在一起还能这么玩”。等到上手写 TSX,又被泛型、类型推断、组件 props 的定义方式折磨得够呛。反过来,Vue 3 全面拥抱 TS 之后,越来越多同学开始在 invest setup 里尝试 JSX 写法,尤其是做组件库、复杂渲染逻辑或动态组件场景,模板有时候真的不够用。

这篇内容我打算把 JSX 和 TSX 整个链路讲透:JSX 到底是个什么产物、编译之后长什么样、TSX 在上面加了哪些约束,以及最近热度很高的“Vue 3 中使用 JSX/TSX”应该怎么落地。无论你是 React 新手、Vue 老手还是全栈工程师,这篇文章都能帮你少踩几个坑。

1. 从模板字符串到 JSX:UI 描述方式的演进

1.1 早年怎么写页面:模板字符串和模板引擎

在 React 还没普及的年代,前端渲染页面最朴素的方式就是字符串拼接。比如 jQuery 时代,要渲染一个列表,通常是这样:

const list = [ { id: 1, name: '张三' }, { id: 2, name: '李四' }, ]; let html = '<ul>'; for (const item of list) { html += `<li>const element = <h1 className="title">Hello, {name}</h1>;

经过编译后,在 React 17 之前的旧版转换方式下,它等价于:

const element = React.createElement( 'h1', { className: 'title' }, 'Hello, ', name );

在 React 17 之后,新的 JSX 转换机制则变成:

import { jsx as _jsx } from 'react/jsx-runtime'; const element = _jsx('h1', { className: 'title', children: ['Hello, ', name] });

看到没有,<h1>不是特殊语法,它最终就是一个函数调用的第一个参数;className="title"就是传给函数的一个对象属性;{name}就是函数的另一个参数。这意味着你完全可以使用 JavaScript 的变量、函数、三元表达式、数组方法去构造视图,不再需要模板引擎去定义“if 指令”“for 指令”。

提示:JSX 的表达式必须放在花括号{}中。{name}里的 name 会被求值,而不会原样输出字符串。

1.3 JSX 为什么用 className 而不是 class

这个问题几乎每个 JSX 新手都会问:为什么 HTML 里写<div class="box">,JSX 里要写<div className="box">?原因很简单:class在 JavaScript 里是保留字,用于定义类。如果 JSX 里直接用class,编译器解析时就会有歧义。同理,<label for="email">在 JSX 中要写成<label htmlFor="email">。

所有 HTML 属性名在 JSX 中都要遵循驼峰命名法:tabindex变成tabIndex,maxlength变成maxLength,onclick变成onClick。本质上是因为 JSX 属性最终会成为 JavaScript 对象的 key,而对象 key 使用驼峰命名是工程上的共识。

2. JSX 的编译机制与核心语法

2.1 谁来把 JSX 编译成 JavaScript

JSX 不是浏览器原生支持的语法,必须经过编译。主流方式有三种:

  • Babel:搭配@babel/preset-react或@babel/plugin-transform-react-jsx,这是最传统的方式,React 项目默认使用。
  • TypeScript 编译器:在tsconfig.json中设置"jsx": "react-jsx",TS 编译器可以直接把.tsx文件中的 JSX 转换掉。
  • esbuild / SWC:新一代构建工具,编译速度快,Vite 底层就用的 esbuild,SWC 则是 Next.js 默认编译器。

以 Vite + React 项目为例,tsconfig.json里常见的配置是:

{ "compilerOptions": { "jsx": "react-jsx", "jsxImportSource": "react" } }

"jsx": "react-jsx"表示使用 React 17 之后的自动运行时转换,编译器会自动从react/jsx-runtime导入jsx函数,你不需要在每个文件顶部import React from 'react'。

2.2 条件渲染:JSX 的“表达式即一切”

模板引擎通常提供专门的指令来做条件渲染,JSX 则直接利用 JavaScript 的逻辑表达式。常见写法有:

{isLoggedIn ? <Dashboard /> : <LoginForm />}
{unreadCount > 0 && <span>{unreadCount} 条未读</span>}
{ status === 'success' ? <SuccessTip /> : status === 'error' ? <ErrorTip /> : <LoadingTip /> }

这里要注意一个细节:&&表达式如果左侧是 0 或空字符串,会因为 JavaScript 的 falsy 特性而渲染出0或空字符串本身。比如{count && <Span />},当 count 为 0 时页面会显示一个“0”。所以更稳妥的写法是{count > 0 && <Span />},或者把值转成布尔值:{!!count && <Span />}。

2.3 列表渲染:map 和 key 的规则

列表渲染在 JSX 里就是调用数组的map方法:

<ul> {items.map((item) => ( <li key={item.id}>{item.name}</li> ))} </ul>

key是 JSX 列表渲染中最容易出问题的点。React 和 Vue 都依赖 key 来高效复用节点,key 必须稳定、唯一。很多人图省事直接用数组下标当 key,这在列表项顺序不变时看起来没问题,但一旦发生插入、删除或排序操作,会导致组件状态错乱。比如下面这个例子,列表前面插了一条数据,子组件内部有input时,输入框的内容会对应到错误的行:

{items.map((item, index) => ( <Row key={index} data={item} /> ))}

注意:只要列表可能发生增删、排序,就不要用 index 作为 key。优先使用业务 id,如果没有唯一 id,再考虑结合字段生成的复合 key。

2.4 为什么说 JSX 是完整 JavaScript 表达力的体现

模板语法设计的出发点是“限制”,因为模板要尽量简单、声明式,开发者不能在模板里写复杂逻辑。JSX 的出发点是“自由”,它允许你在视图描述里直接调用函数、声明变量、做复杂运算。

{ visibleList .filter((item) => item.status === 'active') .sort((a, b) => b.updatedAt - a.updatedAt) .slice(0, 10) .map((item) => ( <Card key={item.id} title={item.title} onClick={() => handleOpen(item.id)} /> )); }

这段逻辑如果放在 Vue 模板里,就需要在computed里写一个计算属性,然后在模板中引用它。两种方式都能实现,但 JSX 让“内联/临时计算”这件事变得非常顺手,尤其适合数据转换链路短、且只存在于渲染上下文的场景。

3. TSX:给 JSX 套上类型安全的外壳

3.1 TSX 和 JSX 的关系

TSX 严格意义上来说是 TypeScript 处理 JSX 语法时的专用文件扩展名。.tsx文件既包含 JSX,又包含 TypeScript 类型标注。编译器处理.tsx文件时,会先进行 JSX 语法转换,再做类型检查。

关键在于:TSX 不是另一门 UI 语言,它是在 JSX 之上的一层类型约束系统。类型标注会在编译后被擦除,运行时和 JSX 没有区别。所以“JSX 和 TSX 详解”这个话题,本质上是两件事:JSX 解决的是“怎么写 UI”,TSX 解决的是“怎么写 UI 才能让编译器帮忙防呆”。

3.2 组件 props 的类型定义

以 React 为例,定义一个带类型的函数组件:

interface ButtonProps { label: string; variant?: 'primary' | 'secondary'; disabled?: boolean; onClick: () => void; } const Button = ({ label, variant = 'primary', disabled = false, onClick }: ButtonProps) => { return ( <button className={`btn btn-${variant}`} disabled={disabled} onClick={onClick} > {label} </button> ); };

这里的ButtonProps会在你使用该组件时提供完整的语法提示。传错类型、漏传必传参数、传了不存在的属性,编译器会直接报错。这是 TSX 相比 JSX 最大的红利:可维护性。在大型项目里,几十上百个组件全靠 JS 注释和自觉去约束 props 是不可想象的。

3.3 泛型组件:让类型跟随数据流动

TSX 的进阶用法是泛型组件。假设我们做一个通用列表组件:

interface ListProps<T> { items: T[]; renderItem: (item: T) => React.ReactNode; } function List<T>({ items, renderItem }: ListProps<T>) { return <>{items.map(renderItem)}</>; }

使用时,类型会自动跟随传入的items推断:

interface User { id: number; name: string; } <List items={users} renderItem={(user) => <span>{user.name}</span>} />

这里renderItem的参数user会被自动推断为User类型,而不是any。在.tsx文件中写泛型箭头函数时,注意要写成<T,>的形式,多一个逗号让解析器知道这是泛型而不是 JSX 标签:

const getRenderValue = <T,>(item: T) => `${item}`;

3.4 事件对象的类型:冷门但实用的细节

在 TSX 中,事件处理函数的参数类型经常被人忽略。React 中 onClick 的事件类型是React.MouseEvent<HTMLButtonElement>,如果不做标注,鼠标事件对象上的currentTarget、clientX等属性就无法获得正确提示。

const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { console.log(event.clientX, event.clientY); }; <button onClick={handleClick}>点击</button>

常见的事件类型还有:React.ChangeEvent<HTMLInputElement>(输入框变化)、React.FormEvent<HTMLFormElement>(表单提交)、React.KeyboardEvent<HTMLInputElement>(键盘事件)。搞不清事件类型时,可以直接把鼠标悬浮在 JSX 的事件属性上,IDE 会给出推断类型,直接复制即可。

4. Vue 3 中使用 JSX/TSX:模板之外的另一种选择

4.1 为什么 Vue 也要拥抱 JSX

很多人以为 JSX 是 React 专属,实际上 Vue 3 提供了完整的 JSX 支持。Vue 官方给出的解释是:模板在多数场景下足够声明式和高效,但遇到复杂动态渲染、函数式组件、以及需要在 render 函数中编写大量分支逻辑时,模板的指令系统会显得笨重。

举一个现实的例子:状态驱动的动态组件。用模板写,通常要包一层<component :is="...">,配合一大堆v-if分支;用 JSX 写,直接就是函数调用和条件表达式的组合,逻辑一目了然。

另外,团队中如果同时有 React 和 Vue 项目,JSX 写法可以降低技术栈切换的心智成本,让经验的迁移更平滑。

4.2 环境配置:Vite 项目接入 JSX

Vue 3 使用 JSX 不需要引入 React,它用的是@vitejs/plugin-vue-jsx插件。安装并配置:

npm install @vitejs/plugin-vue-jsx -D

然后在vite.config.ts中:

import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; import vueJsx from '@vitejs/plugin-vue-jsx'; export default defineConfig({ plugins: [vue(), vueJsx()], });

这样.tsx文件就能在 Vue 项目里直接写了。组件的默认导出方式推荐使用defineComponent:

import { defineComponent, ref } from 'vue'; export default defineComponent({ name: 'Counter', setup() { const count = ref(0); const increment = () => count.value++; return () => ( <div class="counter"> <span>{count.value}</span> <button onClick={increment}>+1</button> </div> ); }, });

4.3 v-model、插槽、指令在 JSX 中怎么映射

Vue 模板中的指令在 JSX 中有对应的写法,但需要显式展开。以v-model为例,模板里是:

<input v-model="value" />

JSX 插件提供了语法糖,可以直接写成:

<input v-model={value} />

如果你不想依赖语法糖,也可以手动展开为“值 + 更新事件”的组合:

<input modelValue={value} onUpdate:modelValue={(newVal) => (value = newVal)} />

插槽在 JSX 中通过v-slots属性实现:

<Child v-slots={{ default: () => <span>默认插槽内容</span>, header: (props) => <div>{props.title}</div>, }} />

v-if在 JSX 里直接用&&或三元表达式替代;v-for用map替代;v-show则可以用style={{ display: show ? '' : 'none' }}或直接调用v-show指令(Vue 3 插件支持)。

4.4 Vue 模板和 JSX 该怎么选,我的建议

我们可以从几个维度对比一下:

维度Vue 模板JSX
条件渲染v-if、v-show 语义化强三元、&&,完全 JS 表达式
循环渲染v-for + keymap + key
双向绑定v-model 语法简洁v-model 语法糖或手动展开
插槽slot 语法结构清晰v-slots 对象写法
动态渲染能力依赖指令和 component函数调用、高阶组件、直接 JS 逻辑
类型推断模板中 props 推断有限TSX 有完整类型检查
学习门槛指令系统需要额外学习本质是 JS 语法

我个人的经验是:普通业务页面、表单页面、低代码配置页面,模板仍然是优先级最高的选择,可读性好,IDE 识别也稳定;但如果你在写组件库、封装通用表格/弹窗、做动态渲染引擎、或者业务里有大量“根据状态组合 UI”的场景,JSX 的效率优势非常明显。两种情况并存才是 Vue 3 项目的常态,不要为了用 JSX 而把简单页面也强行改写。

5. 常见问题与避坑经验

5.1 TSX 中 style 对象的类型报错

写内联样式时,直接写对象字面量经常报类型错误:

<div style={{ background: '#fff', fontSize: 14 }}>

在 React 中,style 对象要求React.CSSProperties类型,数字单位会被自动推断为 px,但字符串'14px'也是合法的。解决方式是显式引入类型或确保对象符合 CSSProperties 结构:

import type { CSSProperties } from 'react'; const style: CSSProperties = { background: '#fff', fontSize: 14, }; <div style={style}>

Vue 3 的 JSX 中,style 类型来自vue包,同样可以import type { CSSProperties } from 'vue'。常见报错是fontSize写成了font-size,注意必须用驼峰。

5.2 ref 在渲染函数中不会自动解包

Vue 3 模板中,ref变量会自动解包,{{ count }}不需要写.value。但 JSX/渲染函数中,返回的是一个普通虚拟节点树,响应式变量的解包需要手动处理:

return () => ( <span>{count.value}</span> );

如果忘了写.value,页面会渲染出[object Object]或者出现类型告警。Vue 3.3+ 提供了响应式语法糖,可以用$ref让 JSX 中也支持自动解包,但需要编译插件配合,日常项目按需使用。

5.3 key 和 ref 的坑:JSX 中 ref 是一个 prop

在 React 的旧版本中,ref是一个特殊属性,不能作为 props 传递给子组件。React 19 中 ref 可以当作普通 prop 传递了。在 Vue 3 的 JSX 中,ref 的用法与模板也不同:

const inputRef = ref<HTMLInputElement | null>(null); return () => ( <input ref={inputRef} /> );

注意这个ref传入的是响应式对象,而不是直接赋 DOM 元素。如果你在自定义组件上使用 ref,需要通过defineExpose暴露组件实例的方法和属性,否则外部无法访问内部状态。

5.4 事件类型推导失败的常见场景

TSX 中最容易让人困惑的是事件参数类型“有时能推断,有时不能”。比如在 Vue 3 中:

<input value={value} onInput={(e) => { // e 的类型可能被推断为 any 或 Event value = (e.target as HTMLInputElement).value; }} />

这里的e经常不会被精确推断为InputEvent,需要手动断言e.target。React 中事件类型相对规范,但自定义事件的类型也需要显式声明。

建议:在团队协作项目中,给常用事件处理函数统一提取类型别名,避免每个文件里都手写MouseEvent<HTMLButtonElement>这种长类型。

5.5 可维护性:JSX 不等于可以随意写逻辑

JSX 提供了强大的表达能力,但不意味着你应该在 JSX 里塞一堆超长内联逻辑。我在实际项目中见过不少人把数据处理全部塞进map回调里,一个 List 渲染写了二十几行,维护者根本分不清哪些是业务计算、哪些是视图结构。

比较好的做法是:JSX 只保留与视图直接相关的条件渲染和循环渲染,复杂的数据处理提前抽成函数或 computed。这和 Vue 模板中把业务逻辑放到 setup 里的思路是一致的。可读性的核心不在于用哪种语法,而在于边界是否清晰。

从 JSX 到 TSX,再到 Vue 3 的 JSX 实践

这几年的前端开发,JSX 和 TSX 已经不只是 React 生态的名词,而是现代组件化编程的通用表达能力之一。Vue 3 对 JSX 的支持让开发者多了一个选择,也让模板与 JSX 的对比变得更有价值。无论你主力技术栈是什么,理解 JSX 在编译层的本质、TSX 提供的类型约束、以及跨框架的语法映射规律,在面对复杂渲染需求时都会更有底气。

我个人在实际项目中保留的习惯是:模板优先用于业务页面,JSX 用于组件封装和动态渲染逻辑;写 TSX 时给每个 props 接口都认真定义类型;事件对象和泛型组件多花一点时间写好类型,后续维护能省下大量排查问题的时间。希望对正在学习 JSX/TSX、或打算在 Vue 3 项目中尝试 JSX 的你有所帮助。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询