一、背景:函数式组件曾经的局限
在 React 16.8 之前(即 Hooks 出现前),React 组件有两种写法:
全屏复制
类型 | 特点 |
---|---|
类组件(Class Component) | 可以使用状态(state)、生命周期方法、ref 等 |
函数式组件(Function Component) | 只能接收 props,返回 JSX,无法管理状态或生命周期 |
❌ 问题来了:
- 函数式组件虽然简洁,但太"被动"了 ------ 它只是一个纯函数。
- 所有复杂逻辑(如数据获取、状态更新、副作用处理)都必须用类组件实现。
- 导致大量"简单 UI + 复杂逻辑"的组件被迫写成类组件,代码臃肿。
👉 比如一个按钮组件,想记录点击次数?必须写成类!
scala
jsx
class ClickCounter extends React.Component {
constructor(props) {
super(props);
this.state = { count: 0 };
}
handleClick = () => {
this.setState({ count: this.state.count + 1 });
};
render() {
return <button onClick={this.handleClick}>Clicked {this.state.count} times</button>;
}
}
而理想中的写法应该是更简洁的函数形式。
🔧 二、Hooks 的诞生:让函数式组件"拥有能力"
React 团队提出了 Hooks 的概念 ------ 允许你在不编写 class 的情况下使用 state 以及其他 React 特性。
✅ 目标:让函数式组件也能做类组件能做的事
于是有了:
useState
→ 使用状态useEffect
→ 处理副作用(如数据请求、订阅、手动 DOM 操作)useContext
→ 访问上下文- 还有数十个内置和自定义 Hook
现在上面的例子可以写成:
javascript
jsx
import { useState } from 'react';
function ClickCounter() {
const [count, setCount] = useState(0);
return (
<button onClick={() => setCount(count + 1)}>
Clicked {count} times
</button>
);
}
✅ 更短、更直观、没有 this
困扰。
🧩 三、为什么不能直接给函数组件加 state?------ 深层原因
你可能会问:
"为什么不直接让函数支持
this.state
,像 class 一样?"
答案是:函数执行完就结束了,无法保存状态
⚠️ 函数执行机制 vs 类实例机制
全屏复制
对比项 | 函数式组件 | 类组件 |
---|---|---|
执行方式 | 每次渲染都重新调用函数 | 实例化一次,多次调用 render() 方法 |
状态存储位置 | 无处可存 | 存在 this.state 上 |
this 是否存在 |
❌ 不存在(或指向错误) | ✅ 存在,指向实例 |
🌰 举例说明:
javascript
js
function Counter() {
let count = 0; // 局部变量
return <div>{count}</div>;
}
每次渲染都会重新执行这个函数,count
被重置为 0,无法持久化。
而类组件:
scala
js
class Counter extends Component {
state = { count: 0 }; // 存在于实例上,不会随 render 丢失
}
所以,普通函数无法天然持有状态。必须有一个外部机制来"记住"上次的状态。
💡 四、Hooks 是如何解决这个问题的?------ 核心原理简析
React 实现了一个 状态记忆系统,它:
- 在首次渲染时,按顺序注册每个 Hook 的初始值;
- 在后续渲染中,按照相同的顺序读取之前保存的状态;
- 利用闭包 + 内部索引数组维护状态(简化理解)
🔄 示例:useState
如何工作?
javascript
jsx
function Counter() {
const [count, setCount] = useState(0); // 第1个 Hook
const [name, setName] = useState('Alice'); // 第2个 Hook
return (
<div>
{count}, {name}
<button onClick={() => setCount(count + 1)}>+</button>
</div>
);
}
React 内部维护一个数组(伪代码):
ini
js
const hooks = [];
let currentIndex = 0;
function useState(initialValue) {
if (hooks[currentIndex] === undefined) {
hooks[currentIndex] = initialValue;
}
const index = currentIndex;
const value = hooks[index];
const setValue = (newValue) => {
hooks[index] = newValue;
reRender();
};
currentIndex++;
return [value, setValue];
}
⚠️ 所以要求:Hooks 必须始终以相同的顺序被调用,不能出现在条件语句中。
🎯 五、Hooks 带来的五大优势(为什么需要它)
全屏复制
优势 | 说明 |
---|---|
✅ 逻辑复用更容易 | 以前靠 HOC 或 Render Props,嵌套深、难维护;现在用自定义 Hook 封装逻辑(如 useFetch , useLocalStorage ) |
✅ 避免 this 的困扰 | 函数组件没有 this ,不再需要绑定事件、担心作用域 |
✅ 更接近函数式编程思想 | 函数 + 纯净副作用控制,更利于测试和推理 |
✅ 组件逻辑组织更灵活 | 不再受限于生命周期函数,可以用多个 useEffect 分离不同逻辑 |
✅ 体积更小、性能更好 | 函数组件比类组件更轻量,Tree-shaking 更友好 |
📦 六、经典例子:对比三种写法
需求:加载用户数据并显示
1. 类组件写法(繁琐)
javascript
jsx
class UserProfile extends Component {
state = { user: null, loading: true };
componentDidMount() {
fetch('/api/user')
.then(res => res.json())
.then(user => this.setState({ user, loading: false }));
}
render() {
const { user, loading } = this.state;
if (loading) return <p>Loading...</p>;
return <p>Hello, {user.name}</p>;
}
}
2. 函数式 + Hooks(清晰简洁)
scss
jsx
function UserProfile() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(data => {
setUser(data);
setLoading(false);
});
}, []); // 空依赖 → 相当于 componentDidMount
if (loading) return <p>Loading...</p>;
return <p>Hello, {user.name}</p>;
}
3. 自定义 Hook(进一步复用)
scss
js
function useUser() {
const [user, setUser] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch('/api/user')
.then(res => res.json())
.then(setUser)
.finally(() => setLoading(false));
}, []);
return { user, loading };
}
然后在任意组件中使用:
javascript
jsx
function UserProfile() {
const { user, loading } = useUser();
if (loading) return <p>Loading...</p>;
return <p>Hello, {user.name}</p>;
}
✅ 真正实现了 UI 与逻辑分离、跨组件复用。
⚠️ 七、Hooks 的规则(也是其设计约束)
为了保证上述机制正常工作,React 规定了两条重要规则:
-
只能在顶层调用 Hooks
- 不能在循环、条件、嵌套函数中调用
- 确保每次渲染调用顺序一致
-
只能在 React 函数组件或自定义 Hook 中调用
- 不能在普通 JavaScript 函数中调用
这些规则由 ESLint 插件 eslint-plugin-react-hooks
自动检查。
✅ 八、总结:为什么函数式组件需要 Hooks?
全屏复制
问题 | 解决方案 | Hooks 的角色 |
---|---|---|
函数组件无法保存状态 | useState |
提供状态管理能力 |
函数组件无法处理副作用 | useEffect |
替代生命周期方法 |
逻辑难以复用 | 自定义 Hook | 封装可复用逻辑单元 |
类组件过于复杂 | 改用函数组件 | 更简洁、易读、易测 |