第 01 课 · JSX 与渲染心智模型
🎯 本课目标
- 说清 JSX 是什么,完成「模板即 JS」的心智转变:不再找指令,直接写 JS
- 把 Vue 指令(v-if / v-for / :class / @click)无损翻译成 JSX 原生写法
- 掌握条件渲染四种写法(&&、三目、变量承载、提前 return),知道什么场景用哪种
- 用
map渲染列表,理解 key 的作用和 index 作 key 在删除场景的 bug - 正确书写
className、style对象(camelCase)与动态 class 拼接 - 记住
{}的边界:null / false / undefined 不渲染,但 0 会渲染
1. JSX 是什么:「模板即 JS」的心智转变
Vue 里你这样写
<script setup lang="ts"> 9
import { ref } from 'vue'
const title = ref('用户管理')
const user = ref<{ name: string } | null>(null)
</script>
<template>
<h1>{{ title }}</h1>
<p v-if="user">{{ user.name }}</p>
<button @click="console.log('clicked')">点我</button>
</template>
Vue 的 template 是一门受限 DSL:只能用指令和表达式,想用 if / for 这些 JS 语句,必须挪到 script 里去(computed、方法、v-for 临时变量)。
React 里这样写
// JSX = 在 JS 里写「长得像 HTML 的结构」,构建时被编译成普通函数调用(语法糖,知道这一点就够)
// 它不是新的模板语言:每个 { } 里面都是真正的 JS
interface User {
name: string
}
function Header({ title, user }: { title: string; user: User | null }) {
// 事件处理就是普通函数
function handleClick() {
console.log('clicked')
}
// if、map、变量、任意 JS 都可以用 —— 因为压根没有「模板语法」的限制
return (
<div>
<h1>{title}</h1>
{user !== null && <p>{user.name}</p>}
<button onClick={handleClick}>点我</button>
</div>
)
}
关键差异与注意点
- 心智转变一句话:Vue 是「先有模板,模板里允许写一点 JS」;React 是「先有 JS,这个 JS 的返回值长得像界面」
- JSX 里不能写
if/for语句,只能写表达式 → 全部换成三目、&&、map class是 JS 保留字,所以用className;label的for变成htmlFor- 属性一律驼峰:
stroke-width→strokeWidth、tabindex→tabIndex(个别 SVG 属性如viewBox保持原样) - 组件名必须首字母大写:
<userCard />会被当成原生标签,<UserCard />才是组件 - 事件传的是函数本身:
onClick={handleClick}正确,onClick={handleClick()}是立即执行(经典手滑)
Vue 指令 → JSX 对照总览
| Vue | React / JSX | 说明 |
|---|---|---|
v-if="ok" | {ok && <A />} | 单分支 |
v-if / v-else | {ok ? <A /> : <B />} | 双分支 |
v-show="ok" | style={{ display: ok ? 'block' : 'none' }} | 一直挂载,只切样式 |
v-for="u in users" | users.map(u => <Row key={u.id} />) | 必须带 key |
:class="{ active: isActive }" | className={`tag ${isActive ? 'tag-active' : ''}`} | 字符串拼接 |
:style="{ color: 'red' }" | style={{ color: 'red' }} | 对象 + 驼峰 |
@click="fn" | onClick={fn} | 传函数引用 |
{{ title }} | {title} | 插值 |
<template> 包多元素 | <>...</>(Fragment) | 不会多出一层 DOM |
<!-- 注释 --> | {/* 注释 */} | 注释也包在花括号里 |
2. {} 花括号:能放什么、不能放什么
Vue 里你这样写
插值里只能写表达式:{{ count + 1 }} 可以,{{ if (ok) {} }} 不行。React 这条规则完全一样,只是 {} 的能力边界更宽 —— 它可以放「一段界面」。
React 里这样写
function ExpressionDemo() {
const count = 2
const user = { name: '张三' }
const ok = true
return (
<div>
{/* 变量、运算、成员访问、模板字符串、函数调用:都可以 */}
<p>{count}</p>
<p>{count + 1}</p>
<p>{user.name.toUpperCase()}</p>
<p>{`共 ${count} 条`}</p>
{/* 三目返回 JSX:{} 里可以放「一段界面」 */}
<p>{ok ? <span className="tag tag-active">启用中</span> : <span className="tag">已停用</span>}</p>
{/* 下面这些是「语句」,不是表达式,直接编译报错 */}
{/* <p>{if (ok) {}}</p> ← 语法错误 */}
{/* <p>{let a = 1}</p> ← 语法错误 */}
{/* 对象不能直接当子节点,会抛错 "Objects are not valid as a React child" */}
{/* <p>{user}</p> ← 必须写 user.name 或 JSON.stringify(user) */}
</div>
)
}
关键差异与注意点
- 记一句:
{}里必须是「有值的表达式」,不能是语句 - 渲染规则要背下来:字符串、数字正常渲染;
null、false、undefined、true什么都不渲染(有意设计,方便ok && <A />) - 但
0和NaN会渲染出来:{arr.length && <List />}在空数组时会画出一个0(Demo 4 演示) - Vue 插值遇到对象会显示成 JSON,React 直接抛错 —— 所以接口返回的对象别整只丢进
{}
3. 条件渲染的四种写法
Vue 里你这样写
<template>
<span v-if="count > 0">{{ count }} 条未读</span>
<span v-else>暂无消息</span>
<div v-show="open">面板</div>
</template>
React 里这样写
function ConditionalDemo({ count, open }: { count: number; open: boolean }) {
// 写法三:变量承载 JSX —— 复杂分支先在函数体里算好
let badge: ReactNode
if (count > 5) {
badge = <span className="tag tag-active">很多未读</span>
} else if (count > 0) {
badge = <span className="tag">{count} 条未读</span>
} else {
badge = <span className="muted">暂无消息</span>
}
return (
<div>
{/* 写法一:&&,单分支「有就渲染」 */}
{count > 0 && <span className="tag tag-active">{count} 条未读</span>}
{/* 写法二:三目,双分支二选一 */}
{count > 0 ? <span>有未读</span> : <span className="muted">暂无消息</span>}
{/* 写法三:变量,JSX 里只放占位符 */}
{badge}
{/* 写法四:提前 return(整个组件级分支) */}
<Greeting count={count} />
{/* v-show 对照:一直挂载,只切 display */}
<div style={{ display: open ? 'block' : 'none' }}>面板内容</div>
</div>
)
}
// 写法四:提前 return —— 分支多、嵌套深时的首选,对应 v-if / v-else-if / v-else 链
function Greeting({ count }: { count: number }) {
if (count === 0) {
return <p className="muted">请先登录</p>
}
if (count > 5) {
return <p>消息太多啦,去收件箱看看</p>
}
return <p>你有 {count} 条新消息</p>
}
关键差异与注意点
- 四种写法的选型经验:单分支用
&&,二选一用三目,三种以上用「变量 + 提前 return」 &&左侧必须是完整布尔表达式:写count > 0 &&,不要写count &&(0 陷阱)- v-show 对应「控制 style.display」:元素始终在 DOM 里;
&&/ 三目对应 v-if:条件不满足时 DOM 里根本没有这个节点 - 三目里分支很长时,宁可抽成子组件或变量,不要在 JSX 里嵌三层三目(code review 必被提)
v-else是靠相邻标签隐式配对的,React 里没有 else 关键字,逻辑全部显式写在表达式里 —— 可读性更依赖你自己组织
4. 列表渲染:map + key
Vue 里你这样写
<template>
<li v-for="todo in todos" :key="todo.id">{{ todo.title }}</li>
</template>
React 里这样写
interface Todo {
id: number
title: string
}
function TodoList({ todos }: { todos: Todo[] }) {
return (
<ul>
{/* map 的返回值就是一组元素,直接当子节点 */}
{todos.map((todo) => (
<li key={todo.id}>
{todo.title}
<button onClick={() => console.log(todo.id)}>完成</button>
</li>
))}
</ul>
)
}
关键差异与注意点
v-for换成map:map 返回 JSX 数组,React 把数组直接当子节点渲染- key 必须放在 map 直接返回的最外层元素上,且在兄弟之间唯一、稳定
- key 不是 prop:子组件里读不到
props.key,需要数据就正常传id={todo.id} - key 的作用:React 靠它把「上一次渲染的元素」和「这一次的元素」对上号,从而复用 DOM 与组件内部状态(diff 复用的身份标识)
- index 作 key 的问题:列表会「重排 / 中间删除」时,下标对不上原来的数据,React 会把 A 的状态安到 B 头上 —— 输入框内容串行、动画错乱、勾选错位(Demo 2 可复现)
- 判断标准:这一项在整个生命周期里会不会被插入、删除、排序?会 → 必须用稳定业务 id
- 数据没有 id 时先造一个(后端返回的 id / crypto.randomUUID()),而不是退回 index
5. className 与 style 对象
Vue 里你这样写
<div :class="['tag', { 'tag-active': isActive }]" :style="{ fontSize: size + 'px' }">标签</div>
React 里这样写
// 手写 clsx:把「真值」class 拼成一个字符串(企业里常用 clsx / classnames 包,思路一致)
function cx(...parts: Array<string | false | null | undefined>): string {
return parts.filter(Boolean).join(' ')
}
function StyleDemo({ isActive, size }: { isActive: boolean; size: number }) {
const tabClass = cx('tag', isActive && 'tag-active')
return (
<div>
{/* className 是字符串:三元 / 模板字符串 / 工具函数三选一 */}
<span className={tabClass}>标签</span>
<span className={`tag ${isActive ? 'tag-active' : ''}`}>标签</span>
{/* style 是对象:属性驼峰,数值自动补 px,百分比自己写 */}
<div
style={{
fontSize: size,
backgroundColor: '#f9fafb',
width: '100%',
marginTop: 8,
}}
>
内容
</div>
</div>
)
}
关键差异与注意点
- Vue 的对象语法
{ active: isActive }没了,React 里就是字符串拼接,企业标准做法是cx()/clsx - style 只能是对象:
font-size→fontSize,background-color→backgroundColor - 数值型样式(width、fontSize)React 自动加 px;
'100%'、'2rem'这类要写字符串 - style 对象每次渲染都是新对象,配合性能优化时要注意(第 07 课展开),静态样式优先走 className
- 本课程不安装 clsx,但企业项目里
clsx(或classnames)几乎必装,见到要认识
6. Fragment 与注释
React 里这样写
function FragmentDemo() {
const list = [1, 2, 3]
return (
<>
{/* JSX 里的注释必须包在花括号里,写成块注释 */}
<h2>标题</h2>
<p>JSX 要求「一个标签只能有一个根节点」,但又不希望多渲染一层 div —— 用 Fragment</p>
{list.map((n) => (
<span key={n}>{n}</span>
))}
</>
)
}
关键差异与注意点
- Vue 的
<template>天然支持多根节点;React 组件必须单根,多元素用<>...</>包起来 <>...</>不会往 DOM 里加任何节点,等价于<template>包裹- Fragment 需要带 key 时用完整写法:
<Fragment key={item.id}>...</Fragment>(从 react 导入 Fragment) - 注释统一用
{/* ... */};在 return 的 JSX 外面(函数体里)就是普通//注释
📊 Vue ↔ React 对照速查表
| 场景 | Vue 3 写法 | React 写法 | 注意点 |
|---|---|---|---|
| 插值 | {{ title }} | {title} | 对象不能直接渲染 |
| 单分支 | v-if="ok" | {ok && <A />} | 左侧要布尔化,防 0 |
| 双分支 | v-if / v-else | {ok ? <A /> : <B />} | 复杂分支抽变量 |
| 多分支 | v-else-if 链 | 变量 + 提前 return | JSX 保持干净 |
| 只切显示 | v-show="ok" | style={{ display: ok ? '' : 'none' }} | 一直挂载 |
| 列表 | v-for="u in list" :key="u.id" | list.map(u => <Row key={u.id} />) | 禁用 index(会增删排序时) |
| class | :class="{ on: isActive }" | className={cx('tag', isActive && 'on')} | 拼接字符串 |
| style | :style="{ fontSize: 14 }" | style={{ fontSize: 14 }} | 驼峰、自动补 px |
| 事件 | @click="fn" | onClick={fn} | 传引用不加括号 |
| 多根节点 | <template> 多根 | <>...</> | 组件必须单根 |
| 注释 | <!-- 注释 --> | {/* 注释 */} | 必须包花括号 |
| 条件隐藏陷阱 | v-if="count"(0 也为假,正常) | {count && <A />} 渲染出 0 | 用 count > 0 |
🏢 企业实战要点
- JSX 里不堆逻辑:一个表达式超过一行(三层三目、链式调用一长串)就抽成变量或子组件。review 里最常见意见:「这段 JSX 已经没法读了,把分支提到函数体里」。
- key 一律用业务 id:团队约定列表数据必须带稳定 id,接口没有就前端生成;code review 看到
key={index}直接打回(除非列表纯静态、永不增删排序)。 - 长 JSX 拆子组件:一个 return 超过 80-100 行、出现三层以上缩进,就拆。拆的标准是「这一块有自己的数据/交互」,不是按行数机械切。
- 组件文件一个默认导出:
UserList.tsx里只放UserList(默认导出)+ 它的私有子组件 / 类型,不要一个文件塞五六个对外组件。 - 样式优先 className,动态值才用 style:整页 style 对象会导致样式无法复用、hover/媒体查询写不了,review 常见意见:「这个搬进 css / CSS Modules」。
{count && <X />}是线上高频 bug:计数、分页 total、数组 length 做 && 条件时务必布尔化,测试环境很难发现(页面悄悄多个 0)。- 数字类 props 做条件前先断类型:接口返回的数量字段可能是
number | undefined,{total && <Pagination total={total} />}在 undefined 时安全、在 0 时炸,统一写total != null && total > 0。 - 命名规范:组件文件大驼峰
UserList.tsx,hooks 文件useUserList.ts,demo/工具小驼峰;JSX 里事件处理器统一handleXxx,传给子组件的回调统一onXxx。
💼 面试高频
1. key 的作用是什么?为什么不能用 index 作 key?
key 是 React diff 时用来匹配「上一次渲染」和「这一次渲染」中同一元素的标识。key 相同,React 就复用这个 DOM 和组件的内部状态,只做必要的更新。用 index 时,删除或排序会让「下标和数据」错位:React 以为第 2 项还是原来那个第 2 项,于是把旧状态(输入框内容、勾选态、动画)安到了新数据头上,表现为串行。静态、永不重排的列表用 index 无害,但企业代码里统一用业务 id 更省心。
2. JSX 和 Vue 的 template 有什么本质区别?
Vue template 是受限 DSL:只能用指令和表达式,框架在编译期做静态分析、自动标记依赖、做细粒度更新优化。JSX 就是 JS:{} 里是任意表达式,if/for 都用原生语法,编译成函数调用后完全由运行时渲染。带来的差异是:React 表达能力更强(能直接把 JSX 存变量、当参数传),但需要手动给 key、手动用 memo 优化;Vue 上手快、默认性能好,但模板里能做的事有限。
3. {count && <Tip />} 有什么问题?怎么修?
&& 返回的是操作数本身:count 为 0 时表达式结果是数字 0,而 React 对数字是照常渲染的,页面上就会多出一个 0。null / false / undefined 才会被 React 忽略。修复方式:{count > 0 && <Tip />}、{!!count && <Tip />} 或 {count ? <Tip /> : null},核心都是把条件变成真正的布尔值。凡是可能为 0 的数值(length、total、剩余次数)做条件都要注意。
4. v-if 和 v-show 在 React 里分别怎么实现?
v-if 对应条件渲染:{ok && <A />},条件为假时节点不在 DOM 里,切换会销毁/重建组件及其状态。v-show 对应样式切换:style={{ display: ok ? 'block' : 'none' }},节点一直在,只改样式。选型标准一样:切换频繁、初始化成本高用 display;条件少变、内容重用条件渲染(省内存、SEO/无障碍也更干净)。
5. JSX 里为什么不能直接写 if 和 for?
JSX 编译后是函数调用 + 对象描述(语法糖),{} 里放的是「会被求值的表达式」,而 if / for 是语句,没有值,语法层面就放不进去。解决办法:把逻辑提到组件函数体(变量、提前 return),或者用能返回值的三目、&&、map、IIFE(不推荐,可读性差)。这也是「模板即 JS」的核心:分支循环这些控制流,全部回到最普通的 JS 写法。
📌 小结 + 下节预告
- JSX 不是模板语言,是「返回界面形状的 JS」:
{}里放表达式,控制流全部用原生 JS - 条件渲染四种写法:
&&、三目、变量承载、提前 return;v-show用 display 模拟 - 列表用
map+ 稳定 key;index 作 key 在增删排序场景会让状态串行 - className 是字符串拼接(
cx()是企业标配思路),style 是驼峰对象 - 0 / NaN 会渲染,null / false / undefined 不渲染 —— 数值条件必须布尔化
下一课讲组件与 Props:函数组件怎么定义、props 为什么只读、Vue 的默认插槽 / 具名插槽 / 作用域插槽在 React 里如何用 children 和「传 JSX 的 props」一一还原,以及 TS 如何替你把 props 类型管起来。
本文深入解析 React JSX 的本质——'模板即 JS'的心智模型,涵盖 JSX 语法规范、花括号表达式规则、四种条件渲染写法(&&/三目/变量/return)、map 列表渲染与 key 的作用机制(强调业务 id 替代 index)、className 与 style 对象的使用规范,以及 Fragment 和注释写法。同时对比 Vue 指令到 JSX 的映射关系,并指出企业级实践要点与面试高频考点。
500

被折叠的 条评论
为什么被折叠?



