引言
在现代 React 开发中,函数组件 + Hooks 已经成为主流范式。它们不仅语法简洁、逻辑清晰,还赋予了函数组件过去只有类组件才拥有的能力:状态管理 、副作用处理 、生命周期控制 等。本文将结合实际代码,以最真实、最完整的代码为蓝本,深入浅出地讲解 React 中两个最核心的 Hooks:useState 和 useEffect,并穿插解释一个至关重要的编程概念------纯函数。
我们将严格引用代码,确保技术细节的准确性;同时用生动的语言和清晰的结构,带你从"知道怎么写"走向"真正理解为什么这样写"。
第一部分:useState ------ 函数组件的状态之源
什么是状态(State)?
在 React 中,状态(State)是驱动 UI 变化的数据 。没有状态,组件就是静态的、死板的。而 useState 就是让函数组件拥有状态的魔法钩子(Hook)。
我们来看 App2.jsx 的完整内容:
javascript
// 状态管理 响应式数据,类似vue的ref
import { useState } from 'react'
export default function App() {
// num是一个状态变量,初始值是1,setNum是更新num的函数
// 数据通过setNum更新,变成了一个数据值,值不是固定的,而是一种状态 State
// hook useState 为程序带来了关键的响应式状态
// 状态是变化的数据,组件的核心是状态
// const [num, setNum] = useState(1)
const [num, setNum] = useState(() =>{
// 如果初始值需要经过复杂的计算得到,用函数来计算
// 一定要是同步函数,不支持异步函数
// 异步的可能不确定有没有执行完,状态一定是确定的,所以不能用异步函数来计算初始值
// 纯函数是指相同输入始终返回相同输出,而且没有副作用的函数
const num1 = 1 + 2
const num2 = 2 + 3
return num1 + num2
})
return (
// <div onClick={() => setNum(num + 1)}>
// 修改函数中可以直接传新的值,也可以传入一个函数
// 这个函数的参数是上一个状态值,返回值是新的状态值
<div onClick={() => setNum((prevNum) => {
console.log(prevNum)
return prevNum + 1
})}>
{num}
</div>
)
}
这段代码展示了 useState 的两种典型用法:
✅ 1. 直接传入初始值(简单场景)
scss
const [num, setNum] = useState(1);
num是当前状态值。setNum是更新状态的函数。- 当调用
setNum(newValue),React 会重新渲染组件,并用新值替换旧值。
✅ 2. 传入初始化函数(复杂计算场景)
dart
const [num, setNum] = useState(() => {
const num1 = 1 + 2
const num2 = 2 + 3
return num1 + num2
})
关键点 :这个函数只在组件首次渲染时执行一次,用于避免重复计算。
但注意注释中的警告:
"一定要是同步函数,不支持异步函数。异步的可能不确定有没有执行完,状态一定是确定的,所以不能用异步函数来计算初始值。"
这是因为 React 需要在渲染前就确定状态的初始值。如果用 async/await,函数会返回一个 Promise,而不是实际的值,这会导致状态变成 Promise {<pending>},显然不是我们想要的。
深入理解:为什么强调"纯函数"?
在 useState 的初始化函数注释中,有一句非常重要的话:
"纯函数是指相同输入始终返回相同输出,而且没有副作用的函数。"
为了理解这句话,我们来看看 1.js 中的例子:
javascript
// function add(nums) {
// nums.push(3)
// return nums.reduce((pre, cur) => pre + cur, 0)
// }
// 改成纯函数 此时add函数没有副作用,作为赋值来使用
const add = function(x,y) {
return x + y
}
const nums = [1, 2]
add(nums) // 副作用
console.log(nums.length)
原始版本(有副作用):
javascript
function add(nums) {
nums.push(3) // 修改了外部数组!
return nums.reduce((pre, cur) => pre + cur, 0)
}
- 这个函数不仅返回结果,还改变了传入的
nums数组。 - 这种行为称为 副作用(Side Effect) :函数除了返回值,还影响了外部环境。
改写后(纯函数):
javascript
const add = function(x, y) {
return x + y
}
- 给定相同的
x和y,永远返回相同的和。 - 不修改任何外部变量,不发起网络请求,不操作 DOM。
- 这就是纯函数。
为什么 React 要求初始化函数是纯函数?
因为 React 依赖可预测性。如果初始化函数有副作用(比如修改全局变量、调用 API、改变传入参数),那么:
- 多次渲染可能导致不一致的状态。
- 在服务端渲染(SSR)或 React.StrictMode 下可能出现 bug。
- 调试变得极其困难。
因此,useState(() => {...}) 中的函数必须是纯函数:无副作用、确定性输出。
如何安全地更新状态?
在 App2.jsx 的 JSX 中,我们看到:
javascript
<div onClick={() => setNum((prevNum) => {
console.log(prevNum)
return prevNum + 1
})}>
{num}
</div>
这里 setNum 接收的是一个函数 ,而非直接的值。这种写法称为 "函数式更新" 。
为什么推荐这样做?
当状态更新依赖于前一个状态时(如计数器 +1),使用函数式更新可以避免竞态条件(race condition)。例如:
scss
// 危险写法(可能出错)
setNum(num + 1);
setNum(num + 1); // 两次都基于同一个旧值!
// 安全写法
setNum(prev => prev + 1);
setNum(prev => prev + 1); // 每次都基于最新值
React 会将 prevNum 作为上一次提交的状态传入,确保你总是基于最新状态进行计算。
第二部分:useEffect ------ 副作用的总指挥
如果说 useState 赋予组件"记忆",那么 useEffect 就赋予组件"行动力"。
我们来看 App.jsx 的完整代码:
javascript
import { useEffect, // 副作用
useState // 响应式状态
} from 'react';
import Demo from './components/Demo';
async function queryData() {
const data = await new Promise(resolve => {
setTimeout(() => {
resolve(666)
}, 2000);
});
return data;
}
export default function App() {
const [num, setNum] = useState(0);
console.log('useEffect 挂载前执行')
// useEffect(() => {
// console.log('useEffect 挂载后执行')
// // 挂载后执行, vue生命周期onMounted
// queryData().then(data => {
// setNum(data);
// })
// }, [])
// 依赖项,只有依赖项变化时,才会执行副作用函数
// }, [1,2,3]) // 依赖项为常数,只有在挂载时执行一次
// }, [1,2,3,new Date()]) // 依赖项为对象,每次渲染时都执行
// useEffect(() => {
// // 挂载时执行一次 onMounted
// // 更新时执行 onUpdated
// console.log(num, 'useEffect num 变化时执行')
// // num 变化时执行, vue生命周期onUpdated
// }, [num])
// useEffect(() => {
// console.log('如果依赖项为空数组,只有在挂载时执行一次')
// }, [])
// useEffect 定时器副作用
// 内存泄漏 每次都在新建定时器,组件卸载时,定时器没有清除,会一直执行
// 解决方法:在副作用函数中返回一个清除函数,组件卸载时,清除定时器
// useEffect(() => {
// const timer = setInterval(() => {
// console.log('定时器副作用执行')
// }, 1000);
// // 重新执行effect之前,会先清除上一次的定时器
// // useEffect return 函数
// // 不清除上一次的定时器,会导致内存泄漏
// return () => {
// console.log('清除定时器')
// clearInterval(timer)
// }
// }, [num])
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
{num % 2 === 0 && <Demo />}
</>
)
}
这段代码通过大量注释,揭示了 useEffect 的三大核心用法。
场景一:模拟 onMounted ------ 组件挂载时执行一次
scss
useEffect(() => {
console.log('useEffect 挂载后执行')
queryData().then(data => {
setNum(data);
})
}, [])
-
依赖项为
[](空数组) :表示该 effect 只在组件挂载后执行一次。 -
相当于 Vue 的
onMounted或类组件的componentDidMount。 -
常用于:
- 发起 API 请求
- 订阅 WebSocket
- 初始化第三方库
⚠️ 注意:虽然
queryData是异步的,但useEffect本身不能是async函数(因为不能返回 Promise)。正确做法是在内部定义 async 函数并调用。
场景二:模拟 onUpdated / watch ------ 依赖变化时执行
scss
useEffect(() => {
console.log(num, 'useEffect num 变化时执行')
}, [num])
-
依赖项为
[num]:只要num的值发生变化(通过Object.is比较),effect 就会重新执行。 -
相当于 Vue 的
watch(num)或类组件的componentDidUpdate(配合条件判断)。 -
适用于:
- 根据搜索关键词发起请求
- 监听路由变化
- 动态调整 UI 行为
❗ 依赖项陷阱
[1, 2, 3]:全是常量 → 只执行一次(等同于[])。[new Date()]:每次渲染都生成新对象 → 每次都会执行(通常不是你想要的!)。- 忘记添加依赖 → 可能导致闭包捕获旧值(stale closure)。
React 官方推荐使用 ESLint 插件 (eslint-plugin-react-hooks)自动检测依赖项是否完整。
场景三:清理副作用 ------ 防止内存泄漏的关键!
这是 useEffect 最容易被忽视、却最重要的特性。
javascript
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器副作用执行')
}, 1000);
return () => {
console.log('清除定时器')
clearInterval(timer)
}
}, [num])
为什么需要清理?
- 每次
num变化,effect 会重新运行,创建新的定时器。 - 如果不清除旧的定时器,它们会继续在后台运行 → 内存泄漏。
- 更严重的是,如果组件卸载了,但定时器还在尝试
setState,React 会报 warning:"Can't perform a React state update on an unmounted component."
清理函数的作用时机:
- 下次 effect 执行前:先清理上一次的副作用。
- 组件卸载时:彻底清理所有资源。
这就是注释所说的:
"return函数 闭包,在下次执行effect前 或 组件卸载时执行"
这个返回的函数就是一个清理函数(cleanup function) ,它捕获了当前 effect 创建的资源(如 timer),形成闭包,确保能正确释放。
第三部分:Hooks 的哲学 ------ 纯函数 vs 副作用
"useEffect 副作用管理 effect副作用对立面是纯函数。对组件来说输入参数,输出jsx。"
这句话揭示了 React 函数组件的设计哲学:
- 理想组件 = 纯函数:给定 props,总是返回相同的 JSX。
- 现实需求 = 副作用:需要发请求、操作 DOM、订阅事件......
- Hooks = 桥梁 :用
useEffect把副作用"隔离"起来,保持组件主体的纯净。
这正是为什么:
useState初始化要用纯函数(保证状态可预测)。useEffect专门处理副作用(不污染渲染逻辑)。
总结:React Hooks 的核心思想
| Hook | 作用 | 关键规则 |
|---|---|---|
useState |
管理状态 | 初始化函数必须是同步纯函数;更新状态推荐使用函数式更新 |
useEffect |
管理副作用 | 依赖项决定执行时机;必须提供清理函数防止内存泄漏 |
通过 App2.jsx,我们学会了如何用 useState 赋予组件状态,并理解了纯函数 的重要性;
通过 App.jsx,我们掌握了 useEffect 的三种经典模式:挂载执行 、依赖更新 、清理资源 ;
通过 1.js,我们明白了副作用的危害 与纯函数的价值 ;
通过 readme.md,我们看到了 Hooks 的整体图景。
项目源码链接:react/hooks· Zou/lesson_zp - 码云 - 开源中国
项目结构:

结语
React Hooks 不仅是一组 API,更是一种思维方式的转变 :将状态逻辑从生命周期中解耦,以声明式的方式组合行为。当你真正理解 useState 的纯函数约束、useEffect 的依赖机制与清理闭包,你就不再只是"会用 Hooks",而是掌握了一种构建健壮、可维护 React 应用的核心能力。
现在,打开你的编辑器,把那些被注释掉的 useEffect 代码取消注释,亲自运行看看控制台输出吧!实践,才是理解 Hooks 的最佳路径。