REACT · Vol.I · LESSON 03 · 核心原语
组件与 Props:前端的「方法」和「参数」
组件就是「函数」,Props 就是「参数」
如果你上一篇文章理解到位了,这一篇会非常轻松。React 里最基本的概念「组件」,用 Java 的话说就是——一个函数。
function UserCard(props) {
return (
<div className="card">
<h3>{props.name}</h3>
<p>{props.title}</p>
</div>
);
}
// 使用:就像调用一个函数
<UserCard name="wan" title="后端工程师" />
它和这个 Java 方法本质上是同一件事:
String userCard(String name, String title) {
return "<h3>" + name + "</h3><p>" + title + "</p>";
}
一个接收「数据」(参数),返回「界面描述」(JSX)的函数,就是 React 组件。就这么简单。
有一个语法细节必须马上记住——组件名必须以大写字母开头:
<UserCard /> // ✅ 大写开头 → React 知道这是你的组件函数
<userCard /> // ❌ 小写开头 → React 当成原生 HTML 标签 <usercard>
<div /> // 小写 → 原生标签,这反而是对的
React 就靠「首字母大小写」区分「自定义组件」和「内置标签」。所以 UserCard 不能叫 userCard,这不是代码风格偏好,是语法要求——就像 Java 的类名必须能过编译器检查一样硬性。
Props 只读:前端的「不可变参数」约定
React 有一条铁律:组件绝不能修改自己的 Props。对应到 Java,就是「方法参数应当是不可变的」。看这个反例:
function UserCard(props) {
props.name = "hacker"; // ❌ 绝对禁止!
return <div>{props.name}</div>;
}
为什么不可以?Java 里我们也有过同样的一课——如果方法 A 调方法 B 时把参数传进去,B 却偷偷改了这个参数,A 内部的其它逻辑就会被「看不见地污染」。这种隐性的数据耦合,是所有大型系统的噩梦。
React 用「Props 只读」这条约定,强制数据流保持单向:数据从父组件流向子组件,永远清晰、可追踪。
你可以把 Props 理解为 Spring 的构造器注入:依赖(数据)在创建时传入,之后不可变更;而组件自己的 state(下一篇讲)则对应实例字段——它是组件内部自持、可以变化的数据。
再借一个词:Props 就像 DTO——跨层传递、只读消费;谁要改数据,谁就得回到数据的「源头」去改(改 state),再把新值一路传下来。
那「我想给 props 加个默认值 / 换个名字」总行吧?行——但做法是「在本地另起炉灶」,而不是改参数本身:
// ✅ 解构出字段,顺便给默认值——完全没碰 props 本身
function UserCard({ name, title = "工程师", avatar }) {
return (
<div className="card">
<img src={avatar} alt={name} />
<h3>{name}</h3>
<p>{title}</p>
</div>
);
}
// 调用方可以省略 title:
<UserCard name="wan" avatar="/wan.png" />
解构默认值对应 Java 里的「方法重载给缺省参数」,让调用方更省事,组件内部更清爽。props 一多,这种写法还能顺带起到「文档化参数列表」的作用——函数签名一眼看清这个组件吃什么数据。
单向数据流 + 组合,拼出整个应用
既然数据只能从父流向子,那整个应用就是一棵「数据流动的树」:
组装(Composition)也简单直接——组件里嵌套组件,像拼积木:
function App() {
const user = { name: "wan", title: "后端工程师" };
return (
<div>
<Header user={user} />
<Profile user={user} />
<Footer />
</div>
);
}
这里藏着一个对 Java 工程师特别亲切的事实:React 没有继承。你永远不会写 class ProfileCard extends BaseCard 这样的东西——复用全靠「组合」:把公共部分抽成组件,然后嵌套使用。这就是那句被说烂但确实成立的话:组合优于继承。你在后端推崇的「多用组合、少用继承」,在前端被贯彻得更彻底。
特殊的孩子:children
有一类 props 不需要显式声明——写进标签「肚子里」的内容,会自动装进一个叫 children 的 prop:
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div className="card-body">{children}</div>
</div>
);
}
// 使用:标签之间的任何东西,都是 children
<Card title="个人信息">
<p>姓名:wan</p>
<p>职业:后端工程师</p>
</Card>
这让你能写出「外壳固定、内容随意」的容器组件——卡片、弹窗、布局框全靠它。对应 Java 里的「模板方法模式」:骨架我定,填充物你给。
把「函数」也当 props 传
props 能传任何值,自然也能传函数。这是子组件「向父组件汇报」的标准通道:
function DeleteButton({ onDelete }) {
// 子组件不知道也不关心「删除」具体怎么做,
// 它只负责在合适的时机调用传进来的函数
return <button onClick={onDelete}>删除</button>;
}
// 父组件掌握实际逻辑:
<DeleteButton onDelete={() => removeUser(user.id)} />
注意数据流依然是单向的:数据向下流(props),动作向上传(回调函数)。子组件不直接改任何东西,它只是「通知」父亲:嘿,该你了。这个模式在第 8 篇 Todo 实战里会大量出现,卷二讲「状态提升」时它还是主角。
什么时候该拆组件?
会写组件之后,下一个问题马上到来:一个页面写成一个巨型组件,还是拆成二十个小件?这条界线画在哪里,直接决定代码的可维护性。给你三条和后端完全同源的判断标准:
| 判断标准 | 后端世界的对应说法 |
|---|---|
| 职责是否单一:这块 JSX 能用一句话描述吗?(「用户头像卡片」「订单状态徽章」) | 单一职责原则——一个 Service 干一件事 |
| 是否会被复用:同样的结构要在两处以上出现? | 抽取公共工具类 / 基类方法 |
| 读起来是否费劲:JSX 超过一屏、嵌套超过三四层、中间隔着大段不相干的逻辑? | 方法超过 80 行就该拆了 |
拆出来的组件之间,用 props 把「接缝」定义清楚,就像微服务之间用接口契约通信。一个可参考的手感:小组件 20~50 行,大组件不超过一屏;拆不出有意义的 props 时,先别硬拆——过早拆分会造成一堆只在一处使用、参数签名却复杂得吓人的「伪组件」,和过早抽象一样有害。
这一篇要背下来的东西
- 组件 = 接收 Props、返回 JSX 的函数;组件名必须大写开头。
- Props 是只读的「参数」(对应构造注入 / DTO),解构 + 默认值是标准姿势。
- 单向数据流:数据向下流(props),动作向上传(回调函数)。
children做容器组件;复用靠组合,没有继承。- 拆组件看三点:职责单一、会被复用、读着费劲。
本章回顾
- 组件 = 函数:输入 Props,输出 JSX。
- Props = 不可变参数:只能读,不能改(对应构造注入)。
- 单向数据流:父 → 子,永远自上而下——这会让你的程序好调试、好推理。
下一篇我们讲 state——组件自己的「实例字段」。Props 是别人给的、不能动;state 是自己的、可以变。这对「一静一动」的搭档凑齐,界面才能真正活起来。
Comments · 评论