在现代 React 开发中,函数组件 + Hooks 已经成为主流开发模式。Hooks 提供了类组件所拥有的状态管理、生命周期控制和副作用处理能力,同时保持了代码的简洁性和可维护性。
本文将以一个完整的示例项目为基础,深入讲解 useState 和 useEffect 的使用方式,特别是重点解析 useEffect 的副作用机制、生命周期模拟以及资源清理逻辑,并通过两个实际组件(App.js 和 Timer.js)来展示 Hooks 在真实场景下的应用。
🧠 函数是一等公民(First-Class Functions)
JavaScript 中的函数是“一等对象”,这意味着:
- 可以赋值给变量
- 可以作为参数传递给其他函数
- 可以作为函数的返回值
- 可以拥有属性和方法(因为函数本质是对象)
这一特性为函数式编程提供了强大支持。React 的函数组件本质上就是一个返回 JSX 的函数,而 Hooks 正是在这样的函数中使用的特殊函数,它们以 use 开头,比如 useState, useEffect, useRef 等。
🧪 useState:状态管理的基础
useState 是我们在函数组件中引入本地状态的 Hook。
示例代码片段(来自 App.js):
const [count, setCount] = useState(0);
const [num, setNumber] = useState(0);
const [repos, setRepos] = useState([]);
const [isTimerOn, setIsTimerOn] = useState(true);
每个调用 useState() 都会创建一个独立的状态单元,并提供更新函数。这样有助于我们保持逻辑清晰、职责单一。
✅ 使用多个
useState而不是将所有状态合并成一个对象,是推荐的最佳实践。
详细解释:
count和setCount:用于计数器1的状态管理。num和setNumber:用于计数器2的状态管理。repos和setRepos:用于存储从 GitHub API 获取的仓库数据。isTimerOn和setIsTimerOn:用于控制<Timer />组件是否显示。
💥 useEffect:副作用处理的核心
如果说 useState 是函数组件的状态之源,那么 useEffect 就是其行为之本。它是 React 中用于处理副作用的核心 Hook。
基本语法结构
useEffect(() => {
// 副作用逻辑
return () => {
// 清理逻辑(可选)
};
}, [依赖项]);
- 第一个参数是一个回调函数,React 会在每次组件渲染后调用它。
- 第二个参数是一个依赖数组,当数组中的值发生变化时,副作用才会再次执行。
- 返回的函数(如果有的话)是清理函数,在组件卸载前或下一次副作用执行前被调用。
结合 App 组件分析 useEffect 的典型用法
示例代码片段(来自 App.js):
useEffect(() => {
console.log('只在组件挂载时运行一次!!!');
const fetchRepos = async () => {
const response = await fetch('https://api.github.com/users/Srierin/repos');
const data = await response.json();
setRepos(data);
};
fetchRepos();
}, []);
解析:
- 该副作用只在组件首次渲染后执行一次(依赖数组为空)。
- 内部定义了一个异步函数
fetchRepos,请求 GitHub API 并更新repos状态。 - 更新后的
repos会触发组件重新渲染,从而在页面上展示获取到的仓库列表。
⚠️ 注意:不能直接将
useEffect设为async,因为useEffect要求返回的是一个清理函数,而不是 Promise。
详细解释:
console.log('只在组件挂载时运行一次!!!'):仅在组件首次加载时打印这条消息。const fetchRepos = async () => {...}:定义一个异步函数,负责发起网络请求并设置响应结果到状态中。fetchRepos();:立即调用这个异步函数。}, []);:空数组表示此useEffect只在组件首次加载时运行。
🕒 Timer 组件详解:useEffect 的完整生命周期控制
示例代码片段(来自 Timer.js):
import { useEffect, useState } from 'react';
const Timer = () => {
const [time, setTime] = useState(0);
console.log('组件函数执行');
console.log('JSX编译');
useEffect(() => {
console.log('组件渲染完了');
const interval = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => {
console.log('组件卸载');
clearInterval(interval);
};
}, []);
return (
<div>已经运行{time}秒</div>
);
};
export default Timer;
执行流程分析:
-
组件函数执行阶段:
console.log('组件函数执行')console.log('JSX编译')- 初始化
time状态为 0 - 构建并返回 JSX 模板
-
useEffect 生命周期控制:
- 组件首次渲染完成后执行副作用函数:
- 启动定时器,每秒钟更新时间
- 控制台输出
'组件渲染完了'
- 返回一个清理函数,在组件卸载前执行:
- 清除定时器,防止内存泄漏
- 控制台输出
'组件卸载'
- 组件首次渲染完成后执行副作用函数:
-
依赖数组的作用:
- 由于依赖数组为空
[],副作用只在组件挂载时执行一次,避免重复启动定时器。
- 由于依赖数组为空
详细解释:
const [time, setTime] = useState(0);:初始化计时器的时间为 0 秒。console.log('组件函数执行');和console.log('JSX编译');:这两行代码帮助我们理解组件函数执行的过程。useEffect(() => {...}, []);:此useEffect在组件挂载时启动一个定时器,每秒增加time状态,并在组件卸载时清除定时器。return <div>已经运行{time}秒</div>;:返回 JSX,展示当前计时器的时间。
🔄 整合 App.js 和 Timer.js
完整代码示例(App.js):
import { useState, useEffect } from 'react';
import './App.css';
import Timer from './components/Timer';
function App() {
const [count, setCount] = useState(0);
const [num, setNumber] = useState(0);
const [repos, setRepos] = useState([]);
const [isTimerOn, setIsTimerOn] = useState(true);
useEffect(() => {
console.log('只在组件挂载时运行一次!!!');
const fetchRepos = async () => {
const response = await fetch('https://api.github.com/users/Srierin/repos');
const data = await response.json();
console.log(data);
setRepos(data);
}
fetchRepos();
}, []);
return (
<>
{count}
<button onClick={() => setCount(count + 1)}>点我</button>
<br />
{num}
<button onClick={() => setNumber(num + 1)}>点我</button>
<ul id="repos">
{repos.map(repo => <li key={repo.id}>{repo.full_name}</li>)}
</ul>
{isTimerOn && <Timer />}
<button onClick={() => setIsTimerOn(!isTimerOn)}>toggle timer</button>
</>
);
}
export default App;
完整代码示例(Timer.js):
import { useEffect, useState } from 'react';
const Timer = () => {
const [time, setTime] = useState(0);
console.log('组件函数执行');
console.log('JSX编译');
useEffect(() => {
console.log('组件渲染完了');
const interval = setInterval(() => {
setTime(prevTime => prevTime + 1);
}, 1000);
return () => {
console.log('组件卸载');
clearInterval(interval);
};
}, []);
return (
<div>已经运行{time}秒</div>
);
};
export default Timer;
🎯 结语
通过这个完整的示例项目,我们不仅掌握了 useState 和 useEffect 的基本用法,还理解了它们在真实项目中的应用场景和注意事项。
React Hooks 让函数组件拥有了类组件的能力,同时也带来了更清晰、更简洁的编程方式。掌握好 useEffect,不仅能写出更健壮的组件逻辑,还能更好地理解和运用 React 的响应式编程模型。
如果你喜欢这篇文章,欢迎点赞、收藏、分享给更多开发者朋友!如果你有更多关于 React Hooks 的疑问,也可以留言交流 😊