3、组件化开发基石:函数组件与类组件、组件的props传递、组件的组合与复用、单向数据流理解
3.1 函数组件与类组件:两种选择,一种初心
React 组件,说白了就是一段能产出 UI 的代码块。我刚入行那会儿,React 还只有类组件。后来 Hooks 出来了,函数组件才真正站上舞台中央。
先看类组件长什么样:
class Welcome extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
再看函数组件:
function Welcome(props) {
return <h1>Hello, {props.name}</h1>;
}
你发现没?函数组件就是一个纯函数,输入 props,输出 JSX。没有 this,没有 render(),干净利落。
核心区别:
- 类组件有生命周期方法(componentDidMount 等)
- 函数组件用 Hooks 实现相同效果(useEffect 等)
- 类组件有 this 指向问题,函数组件没有
- 函数组件更轻量,编译后代码更少
我个人习惯,新项目全部用函数组件 + Hooks。为什么?因为心智负担小。你想想看,类组件里要时刻记着 this.handleClick = this.handleClick.bind(this),烦不烦?
但不是说类组件就该被淘汰。我在维护一个老项目时,里面全是类组件,跑得好好的。迁移成本太高,没必要硬改。
我的建议:新项目用函数组件,老项目保持类组件。别为了炫技而重构。
3.2 组件的 props 传递:数据流动的血管
Props 是 React 组件之间通信的基石。父组件通过 props 把数据传给子组件,子组件通过 props 接收数据。
看个例子:
// 父组件
function App() {
const user = { name: '张三', age: 28 };
return <UserCard user={user} />;
}
// 子组件
function UserCard(props) {
return (
<div>
<h2>{props.user.name}</h2>
<p>年龄:{props.user.age}</p>
</div>
);
}
这里有个细节:props 是只读的。子组件绝对不能修改 props。为什么?因为这会破坏数据流的可预测性。
我曾经踩过的坑:在子组件里直接修改了 props 对象的某个属性,结果父组件的状态也跟着变了,排查了半天才发现是引用传递的问题。记住:props 是单向的,子组件只能读,不能写。
Props 可以传递任何类型的数据:字符串、数字、布尔值、对象、数组,甚至函数。传递函数时,子组件可以通过调用这个函数来间接修改父组件的状态。
function Parent() {
const [count, setCount] = useState(0);
return <Child onIncrement={() => setCount(count + 1)} />;
}
function Child({ onIncrement }) {
return <button onClick={onIncrement}>+1</button>;
}
你看,子组件通过 props 拿到一个函数,点击时调用它。数据还是从父组件流向子组件,但子组件可以「通知」父组件做事情。这就是 React 的设计哲学:数据向下,行为向上。
3.3 组件的组合与复用:搭积木的艺术
React 推崇组合,而不是继承。你想想看,现实中的 UI 不就是由一个个小组件拼起来的吗?
组合的方式有很多种:
| 组合方式 | 说明 | 适用场景 |
|---|---|---|
| children 插槽 | 通过 props.children 渲染子元素 | 布局组件、容器组件 |
| render props | 通过 props 传递渲染函数 | 逻辑复用,如鼠标位置跟踪 |
| 高阶组件(HOC) | 函数接收组件,返回新组件 | 权限校验、日志记录 |
| 自定义 Hook | 提取逻辑到函数中 | 状态逻辑复用 |
我最常用的是 children 插槽。比如一个 Card 组件:
function Card({ title, children }) {
return (
<div className="card">
<h3>{title}</h3>
<div>{children}</div>
</div>
);
}
// 使用
<Card title="用户信息">
<p>姓名:李四</p>
<p>邮箱:lisi@example.com</p>
</Card>
这样 Card 组件就变成了一个通用容器,里面的内容由使用者决定。复用性极高。
经验之谈:设计组件时,先想清楚「这个组件哪些部分是固定的,哪些是可变的」。固定的部分写死在组件里,可变的部分通过 props 或 children 暴露出去。
3.4 单向数据流:React 的定海神针
单向数据流,说白了就是数据只能从父组件流向子组件。子组件不能直接修改父组件的数据,只能通过回调函数「请求」父组件修改。
为什么 React 要这么设计?
- 可预测性:数据流向清晰,出问题容易定位
- 调试方便:每个组件的状态只由自己管理
- 性能优化:可以精确控制哪些组件需要更新
我刚开始学 React 时,总觉得单向数据流很麻烦。以前用 jQuery,想改哪里就改哪里,多爽。但项目一复杂,jQuery 那种双向绑定的方式就变成了噩梦——你根本不知道数据是在哪里被改的。
单向数据流就像一条单行道。你开车时不用担心对面来车,因为所有车都朝一个方向走。这就是 React 的优雅之处。
单向数据流的核心原则:
- 数据从父组件流向子组件(通过 props)
- 子组件通过回调函数通知父组件修改数据
- 状态提升:多个组件共享的状态,放到最近的共同父组件中
举个例子,一个简单的待办事项列表:
function TodoApp() {
const [todos, setTodos] = useState([]);
const addTodo = (text) => {
setTodos([...todos, { id: Date.now(), text }]);
};
return (
<div>
<AddTodo onAdd={addTodo} />
<TodoList todos={todos} />
</div>
);
}
function AddTodo({ onAdd }) {
const [text, setText] = useState('');
return (
<div>
<input value={text} onChange={e => setText(e.target.value)} />
<button onClick={() => { onAdd(text); setText(''); }}>添加</button>
</div>
);
}
function TodoList({ todos }) {
return (
<ul>
{todos.map(todo => <li key={todo.id}>{todo.text}</li>)}
</ul>
);
}
你看,数据流向非常清晰:TodoApp 管理 todos 状态,AddTodo 通过回调添加数据,TodoList 只负责展示。每个组件各司其职,互不干扰。
注意:单向数据流不等于不能有「双向绑定」。React 中常见的受控组件模式(input 的 value + onChange)本质上还是单向数据流——数据由父组件管理,子组件通过回调通知父组件更新。
嗯,到这里你应该明白了。函数组件和类组件是两种实现方式,props 是数据传递的通道,组合是构建 UI 的方法,单向数据流是保证应用稳定的基石。把这四点吃透了,React 的大门就算真正推开了。