React 组件与函数的本质区别
核心心智模型:万物皆函数。组件本质上就是一个 JavaScript 函数,但签了一份特殊的”契约”。
一、两种函数的对比:普通函数 vs 组件
从四个维度彻底区分:命名、输入、输出、调用方式。
1. 命名规范
这是 React 识别身份的唯一标识。
| 类型 | 命名规则 | 示例 | 含义 |
|---|---|---|---|
| 普通函数 | 小驼峰 | formatDate, handleClick | 工具逻辑、事件处理 |
| 组件函数 | 大驼峰 | UserCard, App | UI 界面构建 |
教练提示:React 看到大写字母开头的标签(如
<UserCard />),会当作自定义组件处理;看到小写字母(如<div />),会当作原生 HTML 标签。
2. 输入契约
- 普通函数:极其自由,参数可以是数字、字符串、函数等。
- 组件函数:极其严格,参数必须是一个对象(props),即使不传参也收到空对象
{}。
// ❌ 错误想象:组件直接接收多个参数
function UserCard(name, age) { ... }
// ✅ 正确契约:组件接收一个 props 对象
function UserCard(props) {
return <div>{props.name}</div>
}
// 调用时:<UserCard name="小白" />
// React 帮你把参数打包成对象 { name: "小白" }
3. 输出契约
- 普通函数:返回数字、字符串、布尔值、Promise 等。
- 组件函数:必须返回 JSX 或
null,代表”这段代码在屏幕上长什么样”。
JSX 是什么?一种写在 JavaScript 里的”类 HTML 语法”,让你用写 HTML 的方式描述界面。它不是 React 发明的——JSX 是独立的语法规范,Vue、SolidJS 等框架也用 JSX。只是 React 最早采用、推广得最广。
// JSX 长这样:看起来是 HTML,其实是 JavaScript
<div className="card">
<h1>{title}</h1>
<p>{content}</p>
</div>
核心规则就两条:
- 用
{}插入 JS 表达式:{name}、{1 + 1}、{items.map(...)} - 编译后是函数调用:浏览器不认识 JSX,构建工具(Vite 底层的 SWC/Babel)会把它编译成
React.createElement('div', ...)——你写 JSX,构建工具帮你翻译
文件扩展名:含 JSX 语法的文件必须用 .tsx(TypeScript)或 .jsx(JavaScript),x 就代表含 JSX。TypeScript 项目中这是强制的——tsc 不会对 .ts 文件做 JSX 解析,遇到 < 直接报错。
| 文件内容 | 扩展名 |
|---|---|
| TypeScript + JSX | .tsx |
| TypeScript,无 JSX | .ts |
| JavaScript + JSX | .jsx |
| JavaScript,无 JSX | .js |
4. 调用方式
- 普通函数:你亲自调用,如
const result = doSomething() - 组件函数:React 框架帮你调用,如
<UserCard />只是声明,React 在渲染阶段自动调用
正因为是 React 调用,所以不能随便写
if、for破坏 React 的调度机制。
组件函数在哪执行? 取决于渲染方式:
| 场景 | 执行位置 |
|---|---|
| 客户端渲染(Vite + React) | 浏览器 |
| 服务端渲染(Next.js 等 SSR) | 服务器 + 浏览器都执行 |
SSR 时,组件函数先在服务器跑一遍生成 HTML 发给浏览器,浏览器拿到后再跑一遍接管交互(hydration)。所以组件函数不等于”浏览器端代码”——它是 React 调度的函数,运行环境由渲染方式决定。
这也是为什么 useEffect 只在浏览器端执行——副作用(操作 DOM、发请求)在服务器上没意义,React 会跳过。
二、实战代码对比
// ==========================================
// 场景 A:普通函数
// 用途:处理逻辑、计算数据
// ==========================================
function formatPrice(price: number) {
// 1. 小写开头
return `¥${price.toFixed(2)}`; // 2. 返回字符串
}
// 调用方式:
const priceDisplay = formatPrice(99.9); // 3. 你手动调用
console.log(priceDisplay); // 输出: ¥99.90
// ==========================================
// 场景 B:组件函数
// 用途:渲染 UI
// ==========================================
function ProductCard({ title, price }: { title: string; price: number }) {
// 1. 大写开头
// 2. 接收 props 对象
const displayPrice = formatPrice(price); // 在组件里调用普通函数
return (
// 3. 返回 JSX
<div className="card">
<h2>{title}</h2>
<p>价格:{displayPrice}</p>
</div>
);
}
// 调用方式:
// 4. 像 HTML 标签一样使用,React 框架负责调用
<ProductCard title="T3 Stack 指南" price={199} />;
三、传递数据的语法:引号 vs 大括号
| 语法 | 用途 | 示例 |
|---|---|---|
"" | 传递字符串 | <UserCard name="小白" /> |
{} | 传递非字符串(数字、变量、函数、对象) | <UserCard age={20} /> |
四、组件的”特权”:Hooks
Hooks(如 useState、useEffect)是 React 框架提供的 API,不是浏览器原生的。叫 “Hooks” 是因为它们”钩入”了 React 的渲染生命周期。
只有组件函数才能使用 Hooks——因为 React 内部靠调用顺序来匹配每个 Hook 对应的 state。每渲染一次,组件里的 Hook 必须按相同顺序调用相同次数。普通函数不是 React 调度的,React 没机会追踪它的 Hook 调用顺序,所以会出错。
// ❌ 错误:普通函数里使用 Hook
function myUtil() {
const [count, setCount] = useState(0);
}
// ✅ 正确:组件里使用 Hook
function Counter() {
const [count, setCount] = useState(0);
return <button onClick={() => setCount(count + 1)}>{count}</button>;
}
常用 Hooks:
useState:让函数拥有”记忆”useEffect:处理”副作用”useContext:访问全局上下文
五、组件函数 vs 组件实例
你写了一个组件函数,它只是一个蓝图。你在 JSX 里用一次,React 就造出一个实例。
function Timer() { ... } // 蓝图,不是实例
// 用了两次,造出两个实例
<Timer /> // 实例 A,有自己的 state、ref、props、effect、context、memo、callback……
<Timer /> // 实例 B,有自己的 state、ref、props、effect、context、memo、callback……
就像类和对象的关系——写 class Person 是蓝图,new Person() 一次就是一个实例,每个实例有自己的属性,互不影响。函数组件没有 new,但 React 每次遇到 <Timer />,内部会创建一个 Fiber 节点来”记住”这个实例。
Fiber 节点是 React 内部用来记住每个组件实例的数据结构,大概长这样:
Fiber {
type: Timer // 哪个组件函数
memoizedState: ... // 这个实例的 Hooks 数据(state、ref 等,链表存储)
return: 父Fiber // 树形结构:指向上级
child: 子Fiber // 指向下级
sibling: 兄弟Fiber // 指向同级下一个
}
你不用记这些字段,只需要知道:Fiber 就是 React 给每个组件实例建的档案——组件实例是逻辑概念,Fiber 是这个概念在 React 源码里的具体实现。所有 Fiber 节点组成一棵树,跟组件嵌套关系一一对应,React 靠这棵树做 diff 和调度。
所以两个 <Timer /> 各跑各的定时器、各记各的秒数,就是因为它们是两个独立的实例,各有各的存储空间。不仅是 state、ref、props、effect——所有 Hooks 的数据(context、memo、callback 等)都是挂在实例上的。函数蓝图只是一段代码,实例才是真正带着数据跑的东西。
关键认知:函数是 JS 的,“组件”和”实例”是 React 的概念。
function Timer() {}本身就是一个普通 JS 函数,没有任何特殊之处。是 React 的使用方式——<Timer />编译成React.createElement(Timer),React 拿到这个函数、按自己的规矩调它、给它挂 state 和 ref——让它变成了”组件”。函数还是那个函数,“组件”这个身份是 React 赋予的。
六、T3 Stack 进阶视角:类型安全
用 TypeScript 强化”契约”:
type UserCardProps = {
name: string;
age: number;
isAdmin?: boolean; // 可选属性
};
const UserCard = ({ name, age }: UserCardProps) => {
// ...
};
// TypeScript 会自动检查:
// <UserCard name="小白" age={20} /> ✅ 通过
// <UserCard name="小白" age="20" /> ❌ 报错:age 必须是数字
// <UserCard /> ❌ 报错:缺少 name 和 age
七、总结心法
| 原则 | 规则 |
|---|---|
| 本质 | 组件是函数,函数是组件 |
| 区分 | 看首字母(大写是组件,小写是普通函数) |
| 调用 | 普通函数你调,组件 React 调 |
| 实例 | 组件函数是蓝图,<Component /> 是实例,每个实例各有各的 state/ref/props |
| 数据 | 引号传字符串,大括号传万物 |
- 相关:Props完全指南, Web会话与身份验证