Skip to content
Vue 与 React 组件基础用法:SFC 与 JSX 的编写差异
概述
Vue 的单文件组件与 React 的函数组件都用于将视图封装为独立单元,但二者的组织方式和视图描述语言截然不同。前者将模板、逻辑和样式放在同一个 .vue 文件里,通过指令扩展 HTML;后者则是纯 JavaScript 函数,使用 JSX 在代码中混合标签与表达式。本篇以同一个卡片组件为例,对比两种写法的插值、条件、循环、事件、样式和 props 传递,并梳理模板语法与 JSX 各自的心智模型。
基本概念
SFC 三段式
Vue 的单文件组件(SFC)使用 .vue 扩展名,由三种顶层语言块构成[1][2]:
vue
<template>
<div class="greeting"></div>
</template>
<script setup>
import { ref } from 'vue'
const message = ref('Hello world!')
</script>
<style scoped>
.greeting {
color: #42b883;
}
</style><template>:每个文件最多一个,内容在构建阶段会被预编译为渲染函数,运行时不再解析模板字符串,对性能有利[2]。<script>:作为 ES 模块执行。若使用<script setup>,其内容会被预处理为组件的setup()函数,内部声明的变量和导入的函数自动暴露给模板,无需显式返回。<style>:可以有多个,支持scoped(自动添加唯一属性实现样式隔离)或module(以 CSS Modules 方式暴露样式对象)。开发时<style>会注入为原生标签以支持热更新;最终构建时则抽取合并为独立 CSS 文件[2]。
JSX 函数组件
React 组件没有特殊的文件格式要求。一个函数组件就是接收 props 并返回一段 JSX 的普通 JavaScript 函数[4]:
jsx
function Greeting({ message }) {
return (
<div className="greeting">
<h1>{message}</h1>
</div>
)
}JSX 是 JavaScript 的语法扩展,浏览器不能直接执行。构建工具会将 JSX 编译为 React.createElement 调用,或更现代的自动运行时函数[5]。这与 Vue 模板的编译存在本质区别:Vue 的编译器分析的是 HTML 风格的模板结构,可以从中提取静态信息做编译期优化;JSX 编译器只能将标签转换为函数调用,能获取的结构信息少得多。
基本用法:同一个卡片组件的两种写法
下面用一个用户卡片组件来呈现具体差异。卡片接收 user 数据作为 props,展示头像、姓名、简介,点击按钮触发选中事件。
Vue SFC 版本
vue
<template>
<div class="user-card" :class="{ selected: isSelected }">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<div class="info">
<h3>{{ user.name }}</h3>
<p>{{ user.bio }}</p>
</div>
<button @click="handleSelect">选择</button>
</div>
</template>
<script setup>
const props = defineProps({
user: {
type: Object,
required: true
},
selected: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['select'])
const isSelected = props.selected
function handleSelect() {
emit('select', props.user.id)
}
</script>
<style scoped>
.user-card { /* ... */ }
.user-card.selected { /* ... */ }
.avatar { /* ... */ }
.info { /* ... */ }
</style>React 函数组件版本
jsx
function UserCard({ user, selected, onSelect }) {
const handleClick = () => {
onSelect?.(user.id)
}
return (
<div className={`user-card${selected ? ' selected' : ''}`}>
<img src={user.avatar} alt={user.name} className="avatar" />
<div className="info">
<h3>{user.name}</h3>
<p>{user.bio}</p>
</div>
<button onClick={handleClick}>选择</button>
</div>
)
}CSS 需单独引入或使用 CSS-in-JS 方案,结构与 Vue 版本相同。
两种写法的外观和行为一致,但结构层面的差异已经显现:
- 组织方式:Vue 的模板、逻辑和样式共处一个文件,并借助编译器实现样式隔离。React 组件只包含 JSX 与逻辑,样式需要额外处理。
- 属性命名:JSX 属性采用 JavaScript 的 camelCase 命名(
className、onClick),而不是 HTML 的原始属性名。 - 动态绑定:Vue 在模板中使用
v-bind:(简写:)或{{ }}插入动态值;React 在{}中嵌入任意 JavaScript 表达式,属性直接使用=赋值。
插值与表达式
Vue 模板中的 {{ }} 和 React JSX 中的 {} 都用于嵌入动态内容,但内部能放的内容都是 表达式,语句不行:
vue
<!-- Vue:只能写表达式 -->
<p>{{ user.name.toUpperCase() }}</p>
<p>{{ count + 1 }}</p>jsx
// React:同样只能写表达式
<p>{user.name.toUpperCase()}</p>
<p>{count + 1}</p>const x = 1 或 if 这样的语句在两个环境中都会报错。从表达式能力上看,二者表层的限制是一致的。真正的分歧在于当表达式中需要抽离更复杂逻辑时,Vue 倾向于将逻辑移到计算属性或方法中,React 则可以直接在 JSX 外部用变量承接,或者在 {} 中调用函数。
条件渲染
Vue 提供了 v-if、v-else、v-else-if 和 v-show 指令来控制 DOM 的存在与否或显示状态:
vue
<template>
<p v-if="isAdmin">管理员面板</p>
<p v-else>普通用户视图</p>
<div v-show="isVisible">此节点始终在 DOM 中,切换 display 属性</div>
</template>v-if在条件为假时会销毁组件及其内部状态,为真时重新创建。v-show只是切换display属性,组件实例始终保持。
React 没有对应的指令,条件渲染就是 JavaScript 自己的表达式:
jsx
function Panel({ isAdmin, isVisible }) {
return (
<div>
{isAdmin ? <p>管理员面板</p> : <p>普通用户视图</p>}
{isAdmin && <p>仅管理员可见的内容</p>}
<div style={{ display: isVisible ? undefined : 'none' }}>
始终挂载,切换可见性
</div>
</div>
)
}好处是无需额外学习框架语法;代价是当条件较多时,JSX 内嵌的大量 ?: 和 && 会降低可读性。常见做法是将条件判断提前,在 return 之前就确定要渲染的内容,使 JSX 结构保持清晰。
列表渲染
Vue 使用 v-for 指令:
vue
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>React 直接用数组的 .map() 方法:
jsx
{items.map(item => (
<li key={item.id}>{item.name}</li>
))}两边的 key 作用相同:辅助框架在列表变化时识别每个元素,避免不必要的 DOM 操作。如果需要渲染一组元素而不引入额外包裹节点,Vue 可以在 <template> 上使用 v-for;React 使用 Fragment(或 <> 空标签)。
事件绑定
Vue 通过 v-on(简写 @)绑定事件,并支持修饰符:
vue
<button @click="handleClick">点击</button>
<button @click="count += 1">+1</button>
<form @submit.prevent="onSubmit">提交</form>.prevent 修饰符会自动调用 event.preventDefault(),类似的还有 .stop、.once 等。内联语句 count += 1 也是被允许的。
React 使用 on 前缀的 camelCase 属性:
jsx
<button onClick={handleClick}>点击</button>
<button onClick={() => setCount(c => c + 1)}>+1</button>
<form onSubmit={e => { e.preventDefault(); onSubmit() }}>提交</form>没有修饰符,preventDefault 和 stopPropagation 需要手动调用。内联写法需要包裹成箭头函数,若直接传递 setCount(c + 1) 会在渲染时立即执行。另外,频繁创建新函数引用可能影响子组件的 memo 优化,通常建议将处理器提取为独立函数。
两框架都通过合成事件(或事件代理)机制保证跨浏览器一致性。Vue 模板中可通过 $event 访问原生事件对象;React 的合成事件对象在回调执行完毕后会被回收重用,若需要异步访问事件属性,应先调用 e.persist()(React 17 以前)或提前保存需要的值。
样式与类名的动态绑定
Vue 的 class 和 style 属性可以同时接受静态值和动态绑定,框架内部处理对象合并、前缀补全等工作:
vue
<div class="base-card" :class="{ active: isActive, 'size-large': large }"></div>
<div :class="['base-card', isActive && 'active']"></div>
<div :style="{ color: mainColor, fontSize: size + 'px' }"></div>React 中 className 只是普通字符串,拼接需手动完成;style 接收一个对象,属性为 camelCase,数值会自动加 px:
jsx
<div className={`base-card${isActive ? ' active' : ''}${large ? ' size-large' : ''}`}></div>
<div style={{ color: mainColor, fontSize: size }}></div>可以使用 classnames 这类工具库来简化类名拼接。Vue 的内置处理为编译器提供了更多信息:静态类名可以被提前分析,动态布尔值可以被标记为 patch flag,更新时只检查对应的类名是否变化。React 的 className 字符串对编译器不透明,只知道这是一个表达式,无法区分哪一部分可能变化。
Props 与组件组合
两个框架的 props 都用于父组件向子组件传递数据,且都遵循单向数据流。
Vue(<script setup>):
vue
<!-- 父组件 -->
<UserCard :user="currentUser" :selected="true" @select="handleSelect" />
<!-- 子组件 -->
<script setup>
const props = defineProps({
user: Object,
selected: Boolean
})
const emit = defineEmits(['select'])
</script>React:
jsx
// 父组件
<UserCard user={currentUser} selected={true} onSelect={handleSelect} />
// 子组件
function UserCard({ user, selected, onSelect }) { /* ... */ }Vue 的 defineProps 与 defineEmits 是编译器宏,编译后会被替换为运行时代码,不需要导入。子组件通过 emit('事件名', 参数) 向父组件发送事件,父组件用 @事件名 监听。React 中函数组件直接解构 props 参数,事件处理器以回调函数形式传入,命名上常用 on 前缀(社区惯例,非强制)。子组件调用 onSelect(id) 即等效于 Vue 的 emit('select', id)。
Vue 会在开发环境下对直接修改 props 的行为发出警告;React 本身不检测此类修改,但修改传入的值不会触发父组件重新渲染,可能导致状态不一致。
模板与 JSX 的本质差异
模板:受约束的表达
Vue 的模板建立在 HTML 之上,通过 v-if、v-for、v-bind 等指令扩展其能力,但始终停留在声明式结构的范畴。模板中不能声明变量、不能写赋值语句、不能使用 if/else 块语句。复杂逻辑必须移到计算属性、方法或子组件中。
这种约束将逻辑“逼出”模板,使模板专注于描述 UI 结构。工程上的收益体现在两方面:
- 编译优化:由于模板语法受控,编译器可以区分静态内容与动态绑定,实施静态提升、patch flag、树结构打平等优化,减少运行时的虚拟 DOM 比较开销[5]。
- 可读性:遵循 SFC 三段式的团队中,任何人预期模板里不会出现复杂逻辑——如果有,就意味着写法需要调整。这种一致性降低了理解成本。
JSX:完整的编程能力
JSX 本质是函数调用的语法糖,<div className="foo"> 会被编译为 jsx('div', {className: 'foo'})。因此,JSX 完全融入 JavaScript 的表达体系:条件渲染是 ?: 或 &&,列表渲染是 .map(),任何一段 JSX 都可以赋值给变量、作为参数传递、放在数组里。
这种灵活性也意味着编译器很难从结构中提取足够的信息来做静态优化。React 更依赖组件重新执行、虚拟 DOM diff 和 memo 来避免不必要的更新。在实际项目中,JSX 的表达能力需要工程手段来平衡——把复杂的判断和循环提前处理好,在 return 中保持 JSX 的简洁,这与 Vue 将逻辑从模板中抽离的做法殊途同归。
取舍
Vue 也支持渲染函数与 JSX[3]:当组件需要极端的动态化(例如根据运行时数据决定渲染 h1 还是 h2)时,模板的表达能力不足,就该用渲染函数。React 开发者则可以通过代码规范、lint 规则来限制 JSX 中的表达式复杂度,制造出类似“模板”的清晰结构。两种方式的选择不在于好坏,而在于:编写视图时,是更接受一个能帮助编译器优化的语法约束,还是让视图描述完全融入 JavaScript 的表达体系,再由团队自己掌控复杂度。
