在 React 的函数组件世界里,Hooks 是灵魂所在。正如你的 readme.md 中所言,"以 use 开头的函数,都是 React 提供的 Hooks,用于在函数组件中使用状态和其他 React 功能。"其中,useState 解决了"数据如何驱动视图"的问题,而 useEffect 则解决了"如何处理纯函数之外的副作用"的问题。
第一部分:useState------赋予组件"记忆"的能力
1. 响应式状态的本质
在 React 之前,普通变量(如 let count = 0)的变化无法触发 UI 的重新渲染。useState 的出现,为程序员带来了关键的响应式状态 ,状态就是变化的数据,组件的核心是状态。
最典型的定义方式:
JavaScript
const [count, setCount] = useState(1);
- 数组解构 :
useState返回一个长度为 2 的数组。第一项是当前状态的值,第二项是更新该状态的函数。 - 确定性:初始化时,状态必须是确定的,肯定的。
2. 惰性初始化(Lazy Initialization)
当你需要通过复杂计算来确定初始值时,可以向 useState 传入一个纯函数:
什么是纯函数?
纯函数是指对于相同的输入始终返回相同输出,且没有副作用(如修改外部状态或依赖外部可变数据)的函数。
JavaScript
const [count, setCount] = useState(() => {
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2;
});
为什么要用函数?
如果初始值计算开销很大(比如读取本地缓存、循环计算),直接写在 useState(expensive()) 中会导致每次组件重新渲染时都执行该计算。而传入函数,React 只会在组件初次挂载时执行它一次。
注意,这个函数必须是同步的,不支持异步的,异步的初始会带来不确定性,而状态必须是可预测且确定的。
3. 更新函数的两种姿势
更新状态的两种逻辑:
- 直接传递新值 :
setCount(count + 1)。 - 传递更新函数 :
setNum(prevNum => prevNum + 1)。
核心区别:
使用 prevNum => prevNum + 1 是为了安全地基于上一次的值更新。由于 React 的状态更新可能是异步的,或者在闭包环境中,直接引用 count 可能会拿到旧值。使用回调函数形式可以确保你拿到的 prevNum 永远是内存中最新的状态。
第二部分:useEffect------掌控副作用的艺术
1. 什么是副作用(Side Effect)?
副作用的对立面就是纯函数。
- 纯函数:相同输入永远得到相同输出,无副作用。
- 副作用:指那些不直接参与 UI 计算的操作,如网络请求、手动修改 DOM、设置定时器、记录日志等。
useEffect 的设计初衷,就是为这些"不纯"的操作提供一个安全的避风港。
2. 依赖项数组(Dependency Array)的奥秘
useEffect 的第二个参数决定了副作用何时运行,这是初学者最容易困惑的地方:
| 依赖参数 | 执行时机 | 对应生命周期 |
|---|---|---|
| 不传 | 每次组件渲染(render)后都执行 | 持续更新 |
空数组 [] |
仅在组件挂载(Mount)后执行一次 | onMounted |
有值 [num] |
挂载后执行,且当 num 变化时重新执行 |
onUpdated |
3. 清理函数(Cleanup):防止内存泄漏
这是 App.jsx 和 Demo.jsx 中最精彩的部分。
JavaScript
useEffect(() => {
const timer = setInterval(() => { console.log(num); }, 1000);
return () => {
clearInterval(timer); // 清理函数
}
}, [num]);
为什么要 Cleanup?
- 重复创建问题 :如果不清除,每次
num变化都会产生一个新的定时器,旧的定时器依然在后台运行。 - 内存泄漏 :在
Demo.jsx中,如果组件卸载了(num变成奇数),但定时器没关,它会尝试操作已经不存在的逻辑,导致性能下降甚至崩溃。
执行时机:
-
清理函数会在下一次副作用执行前 被调用,如
App.jsx中加的[num]依赖,在num发生改变时,副作用重新执行。 -
清理函数会在组件卸载前 被调用,如
Demo.jsx中的[]依赖,副作用仅在组件挂载后执行一次,但在num为奇数时,Demo子组件会卸载。
第三部分:实战代码深度剖析
案例 1:App.jsx 中的动态监听
在 App.jsx 中,useEffect 监听了 [num]。当用户点击 div 使 num 增加时:
-
React 发现
num变了。 -
React 先调用上一次 Effect 返回的
remove清理函数,执行clearInterval。 -
React 执行新的 Effect,开启一个新的定时器,打印最新的 num。
这就是为什么你在控制台能看到"实时更新"的数字,且不会堆积多个定时器。
案例 2:Demo.jsx 的挂载与卸载
JavaScript
useEffect(() => {
console.log('123123'); // 仅在 Demo 出现时打印一次
return () => { console.log('remove'); } // 仅在 Demo 消失时执行
}, []);
在主组件中:{num % 2 === 0 && <Demo />}。
- 当
num从 0 变 1 时,条件为假,Demo组件被销毁。 - 此时 React 自动触发
Demo内部useEffect的return函数。 - 结论 :这完美模拟了 Vue 中的
onMounted和onUnmounted。
第四部分:Hooks 的使用准则
为了确保这些钩子正常工作,必须遵守 React 的两条金科玉律:
- 只在最顶层使用 Hooks:不要在循环、条件判断或嵌套函数中调用 Hook。这保证了每次渲染时 Hook 的调用顺序一致,React 才能正确关联状态。
- 只在 React 函数中调用 Hooks :不要在普通的 JS 函数中调用,除非是你自定义的 Hook(以
use开头)。
第五部分:总结与感悟
useState 和 useEffect 的组合,体现了 React 声明式编程的思想:
- 你只需要声明: "当数据是这样时,界面应该长这样" (
useState)。 - 你只需要声明: "当数据变化时,我需要同步做这些额外的事" (
useEffect)。
通过提供清理机制,React 强迫开发者思考资源的生命周期,从而编写出更健壮、无泄漏的前端应用。
第六部分:App.jsx和Demo.jsx源码
这个 App.jsx + Demo.jsx 应用主要做了两件事:
- 点击一个数字,让它加 1。
- 当数字是偶数时,显示一个叫 的组件;奇数时隐藏它。
- 同时,用定时器每隔 1 秒打印当前数字,并确保不会造成内存泄漏(比如旧的定时器没关掉)。
js
// App.jsx
import { useState, useEffect } from 'react';
import Demo from './components/Demo.jsx';
async function queryData() {
const data = await new Promise(resolve => {
setTimeout(() => {
resolve(666);
}, 2000)
});
return data;
}
export default function App() {
const [num, setNum] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
console.log(num);
}, 1000)
return () => {
console.log('remove');
clearInterval(timer);
}
}, [num])
return (
<>
<div onClick={() => setNum(prevNum => prevNum + 1)}>{num}</div>
{num % 2 === 0 && <Demo />}
</>
)
}
主组件 App
每当num发生变化时,这个 useEffect 会先清理上一次的副作用(通过return函数),再执行新的副作用。
它启动了一个每秒打印当前 num 的定时器。
返回的函数(return () => {...})就是清理函数,会在下一次 useEffect 执行前,或者组件卸载时自动调用。
为什么需要清理?
如果不清理,每次 num 变化都会新建一个定时器,但旧的还在跑! 比如:num=0 时开了一个定时器,num=1 时又开一个......最后可能有 10 个定时器同时打印,造成内存泄漏或逻辑混乱。
所以:只要用了 setInterval、setTimeout、监听事件等,几乎都要写清理函数。
js
import {useEffect, useState} from 'react';
export default function Demo() {
useEffect(() => {
const timer = setInterval(() => {
console.log('timer');
}, 1000)
// 当 num 是偶数时,子组件还在页面上,没有卸载 React 不会执行return清理函数
// 而当 num 变为奇数时,<Demo /> 被移除 → React 自动调用 return () => {...} → 清理定时器。
return () => {
console.log('remove');
clearInterval(timer);
}
}, [])
return (
<div>
偶数Demo
</div>
)
}
子组件 Demo
useEffect(..., []):只在组件第一次挂载时执行一次(类似 Vue 的 onMounted)。
当 num 变成奇数时, 被移除 → React 自动调用 return () => {...} → 清理定时器。
这样就避免了:组件都消失了,定时器还在后台跑!
这就是 React 的"生命周期"思想:挂载 onMounted → 更新 onUpdated→ 卸载 onUnmounted,而 useEffect + 清理函数 能覆盖全部阶段。