很高兴能和你一起探讨 React 的世界。如果你是从传统的 HTML/CSS/JS 走过来的,或者刚从 Vue 转型,那么 Hooks 绝对是让你爱上 React 的那个“瞬间”。
在 React 的世界里,Hooks(以 use 开头的函数)就像是给函数组件穿上的“钢铁侠战甲”,让原本只能负责渲染 UI 的简单函数,拥有了状态管理、生命周期钩子等超能力。
今天,我们就带上你的好奇心,深入拆解 React 中最核心的两个 Hook:useState 与 useEffect。
一、 为什么是 Hooks?
在 Hooks 出现之前,React 的状态逻辑必须写在 Class(类组件)里。但是类组件太重了,this 的指向问题经常让人头秃。
Hooks 的出现让 函数组件(Function Component) 变成了主流。它的风格非常“原生 JS”,代码看起来更简洁、更直观。
核心法则: 只有在 React 函数组件的最顶层或自定义 Hooks 中调用 Hooks,不要在循环、条件判断或嵌套函数中调用它们。
二、 useState:给组件注入“记忆”
组件本质上是一个函数。普通函数执行完,内部变量就销毁了。但 UI 需要“记忆”用户操作后的数据,这就是 State(状态) ,不固定的值就是状态,而状态是组件的核心。。
1. 基础用法与延迟初始化
看下面这段代码,我们先聊聊 useState 的初始化:
JavaScript
import { useState } from 'react'
export default function App() {
// 1. 初始化传入一个纯函数 (Lazy Initializer)
// 关键点:如果初始值需要经过复杂的计算,不要直接写在 useState(calc()),
// 这样每次组件重新渲染都会执行计算。传入一个纯函数,它只会在【首次挂载】时执行一次。
const [num, setNum] = useState(() => {
// 这里必须是同步函数,不能是 async/await
// 纯函数:相同输入始终返回相同输出,无副作用
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 最终初始值是 10
});
return (
<div onClick={() => setNum(num + 1)}>
{num}
</div>
)
}
一个很关键的点:初始化传入一个纯函数。
什么是纯函数(Pure Function)?
简单来说,纯函数就是:相同的输入永远得到相同的输出,且没有副作用(不修改外部变量,不发请求)。
🤔 深度思考:为什么初始化不能是异步的?
React 的渲染过程必须是确定且连续的。如果初始状态是异步获取的(比如 fetch),在数据回来之前,React 不知道该渲染什么。因此,异步数据请求通常放在 useEffect 中,而 useState 只负责同步状态的定义。
2. 更新状态的两种姿势
当你调用 setNum 时,你有两种选择:
-
直接传值:
setNum(num + 1)。简单直观,但在处理高频连点或闭包场景时,可能会拿不到最新的num。 -
传入函数(推荐):
// 关键点:setState 也可以传入一个函数,参数是上一次的最新的 state (prevNum)
<div onClick={() => setNum((prevNum) => {
console.log("旧状态是:", prevNum);
return prevNum + 1;
})}>
{num}
</div>
专业术语:响应式状态
当你调用 setNum 时,React 会察觉到数据的变化,并自动重新触发函数组件的执行(重新渲染)。这就是 React 的响应式魔法。
三、 useEffect:处理那些“不安分”的副作用
在函数式编程中,我们追求 “纯函数”。
- 纯函数: 给它 ,它永远返回 ,且不修改外部变量(比如不发请求、不改 DOM)。
- 副作用 (Side Effect): 凡是函数执行过程中对外部环境产生影响的操作,都叫副作用。比如:修改全局变量、发送网络请求、设置定时器、直接操作 DOM。
useEffect 就是 React 专门用来安置这些“副作用”的避风港。
1. 它的三张面孔(依赖项的区别)
useEffect(callback, dependencies),第二个参数数组 [] 决定了副作用执行的时机。
情况 A:不传依赖(每次都跑)
useEffect(() => {
console.log('我是“劳模”,每次组件渲染(挂载+更新)后我都会执行');
});
情况 B:空数组依赖 [](只跑一次)
相当于类组件的 componentDidMount (onMounted)。
JavaScript
useEffect(() => {
console.log('xxx');
// 关键点:异步请求数据通常写在这里
queryData().then(data => {
setNum(data);
})
}, []); // 只在挂载完成时执行一次
情况 C:有依赖项 [num](按需执行)
相当于 componentDidUpdate (onUpdated)。
JavaScript
useEffect(() => {
// 关键点:挂载时会执行一次,之后每当 num 发生变化时,这里都会再次执行
console.log(num, 'zzz');
}, [num]); // 只有 num 变了,我才动
四、 核心重难点:生命周期与清理函数 (Cleanup)
很多新手会忽视 useEffect 的返回值。其实,这个 return 才是防止内存泄漏的关键。
1. 为什么要 Return?
看这个复杂的例子:
JavaScript
useEffect(() => {
console.log('effect 执行了');
// 关键点:设置定时器是一个典型的副作用
const timer = setInterval(() => {
console.log("当前的 num 是:", num);
}, 1000);
// 关键点:useEffect 的 return 函数是一个“闭包”
// 它会在两个时机执行:
// 1. 下一次 effect 执行之前
// 2. 组件卸载 (Unmount) 时
return () => {
console.log('remove: 清理旧的副作用,防止内存泄漏');
clearInterval(timer); // 重新执行前,先关掉旧的定时器
}
}, [num]);
执行流程拆解:
num从 0 变成 1。- React 发现依赖项
num变了。 - 第一步: 执行上一次 Effect 留下的
return函数(清除旧定时器)。 - 第二步: 执行新的 Effect 函数(开启新定时器)。
如果没有这个 return,每次 num 改变你都会新开一个定时器,而旧的定时器还在后台默默运行。很快,你的浏览器就会卡死!
2. 组件卸载时的实战演练
看我们定义的那个 Demo 组件:
JavaScript
export default function Demo() {
useEffect(() => {
console.log('Demo 挂载了');
const timer = setInterval(() => {
console.log("Demo timer running...");
}, 1000);
return () => {
// 关键点:当 Demo 组件在父组件中因为条件判断被销毁时
// 这个 return 函数会被调用,回收资源
console.log('Demo 卸载,清理定时器');
clearInterval(timer);
}
}, []) // 注意这里是空数组,意味着 return 只在组件真正销毁时执行
return <div>偶数才会显示我哦</div>
}
在父组件中:{ num % 2 === 0 && <Demo /> }。
当 num 是奇数时,Demo 组件会从页面上消失。这时,useEffect 的清理函数会自动触发。这在处理 Socket 连接、全局事件监听、第三方库销毁时非常重要。
五、 纯函数 vs 副作用:技术深度思考
作为开发者,我们要时刻警惕代码中的“隐形破坏”。
什么是破坏?看这个例子:
JavaScript
function add(nums) {
nums.push(3); // 关键点:这就是副作用!你修改了外部传入的引用类型
return nums.reduce((pre, cur) => pre + cur, 0);
}
const nums = [1, 2];
add(nums);
console.log(nums.length); // 变成了 3!这就是不可预测性。
在 React 中,我们希望 组件渲染过程是纯净的。
- 输入 Props,返回 JSX。
- 不要在渲染函数体(Function Body)内直接修改变量、发请求。
- 把所有不确定的事情,统统交给
useEffect。
六、 总结与最佳实践
写好 React Hooks 的口诀:
-
useState: 用来存“变”的数据。复杂初始值用函数传参,更新状态用回调函数获取
prev值。 -
useEffect: 用来处理“外”的事情。
[]:挂载时跑一次(请求数据)。[dep]:依赖变了跑一次(联动更新)。return () => {}:随手关灯、随手关门(清理副作用)。
🎁 给新手的避坑指南:
- 不要在
useEffect里修改作为依赖项的那个状态(除非有终止条件),否则会陷入 死循环! - 异步请求的数据,如果要在页面显示,一定要存进
useState。