React01:JSX 与渲染心智模型

本文深入解析 React JSX 的本质——'模板即 JS'的心智模型,涵盖 JSX 语法规范、花括号表达式规则、四种条件渲染写法(&&/三目/变量/return)、map 列表渲染与 key 的作用机制(强调业务 id 替代 index)、className 与 style 对象的使用规范,以及 Fragment 和注释写法。同时对比 Vue 指令到 JSX 的映射关系,并指出企业级实践要点与面试高频考点。

第 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 对照总览

VueReact / 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 链变量 + 提前 returnJSX 保持干净
只切显示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

🏢 企业实战要点

  1. JSX 里不堆逻辑:一个表达式超过一行(三层三目、链式调用一长串)就抽成变量或子组件。review 里最常见意见:「这段 JSX 已经没法读了,把分支提到函数体里」。
  2. key 一律用业务 id:团队约定列表数据必须带稳定 id,接口没有就前端生成;code review 看到 key={index} 直接打回(除非列表纯静态、永不增删排序)。
  3. 长 JSX 拆子组件:一个 return 超过 80-100 行、出现三层以上缩进,就拆。拆的标准是「这一块有自己的数据/交互」,不是按行数机械切。
  4. 组件文件一个默认导出:UserList.tsx 里只放 UserList(默认导出)+ 它的私有子组件 / 类型,不要一个文件塞五六个对外组件。
  5. 样式优先 className,动态值才用 style:整页 style 对象会导致样式无法复用、hover/媒体查询写不了,review 常见意见:「这个搬进 css / CSS Modules」。
  6. {count && <X />} 是线上高频 bug:计数、分页 total、数组 length 做 && 条件时务必布尔化,测试环境很难发现(页面悄悄多个 0)。
  7. 数字类 props 做条件前先断类型:接口返回的数量字段可能是 number | undefined,{total && <Pagination total={total} />} 在 undefined 时安全、在 0 时炸,统一写 total != null && total > 0。
  8. 命名规范:组件文件大驼峰 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 类型管起来。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

个

红包个数最小为10个

元

红包金额最低5元

当前余额3.43元 前往充值 >
需支付:10.00元
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值