React 之 Hooks:让函数组件拥有 "超能力" 的魔法
在 React 的世界里,Hooks 就像一把打开函数组件潜能的钥匙。从 Class 组件到函数组件的演进中,Hooks 的出现彻底改变了 React 的开发模式。今天我们就深入探索 Hooks 的奥秘,从基础概念到实战技巧,带你全面掌握这一 React 核心特性。
一、初识 Hooks 三问
1.什么是 Hooks?
Hooks 是 "以use开头的函数",是 React 提供的一套 API,让函数组件能够拥有原本只有 Class 组件才有的状态(State)和生命周期特性。简单来说,Hooks 就像给函数组件装上了 "发动机",让它从 "静态展示" 升级为 "动态交互"。
比如我们熟悉的useState和useEffect,都是 React 内置的 Hooks。它们的出现让 React 代码更贴近原生 JavaScript,函数式风格的写法也让逻辑更清晰。
2.早期没有 Hooks 有什么问题?
在 Hooks 出现之前,React 开发者主要依赖 Class 组件处理状态和副作用,但这带来了两个明显痛点:
- 生命周期函数混乱 :一个 Class 组件的
componentDidMount、componentDidUpdate里可能混杂着数据请求、事件监听、定时器等多种逻辑,维护时需要在多个生命周期间跳来跳去。 - 函数组件能力有限:纯函数组件只能接收 props(属性) 并返回 JSX,无法拥有自己的状态和副作用处理能力,限制了其应用场景。
3.Hooks 产生的原因?
Hooks 的诞生就是为了解决上述问题:
- 让函数组件具备状态管理和副作用处理能力,统一组件写法(推荐函数组件 + Hooks)。
- 使组件逻辑复用更简单,通过自定义 Hooks 可以轻松提取和共享逻辑。
- 让相关逻辑聚合在一起(比如数据请求和清理操作),而非分散在不同生命周期中。
用一句话总结:Hooks 让 React 组件的逻辑更清晰、复用更简单、代码更易维护。
二、纯函数与副作用
理解纯函数与副作用,是掌握 Hooks 的基础 ------ 尤其是useEffect的设计核心就围绕这两个概念展开。
1.什么是纯函数?
纯函数是满足两个条件的函数:
- 相同输入一定产生相同输出:只要传入的参数不变,返回结果就绝对一致。
- 无副作用:不会修改函数外部的变量、不依赖外部状态、不产生网络请求 / 定时器等 "额外操作"。
代码示例:
javascript
// 纯函数示例
const add = function(x, y) {
// 仅依赖输入参数x和y,无外部依赖
// 运算过程中不会修改外部变量,也没有网络请求、定时器等操作
return x + y;
}
// 调用结果可预测:add(2,3)永远返回5,add(1,1)永远返回2
console.log(add(2, 3)); // 5
console.log(add(2, 3)); // 还是5
这个add函数就是典型的纯函数:它的输出完全由输入决定,且不会对外部环境造成任何影响。
2.什么是副作用?
副作用(Side Effect)是指函数在执行过程中,除了返回结果外,还对外部环境产生了额外影响,或者依赖外部环境的状态。
常见的副作用包括:
- 修改函数外部的变量(如数组、对象)
- 发送网络请求(fetch/axios)
- 操作 DOM(添加 / 删除元素)
- 设置定时器 / 计时器(setInterval/setTimeout)
- 注册 / 移除事件监听
代码示例:
javascript
// 副作用示例
function add(nums) {
nums.push(3); // 修改了外部传入的数组(副作用:改变外部状态)
return nums.reduce((pre, cur) => pre + cur, 0);
}
const nums = [1, 2];
add(nums);
console.log(nums); // [1,2,3](外部数组被修改,结果不可预测)
这个add函数就有副作用:它修改了传入的nums数组(外部变量),导致调用后nums本身发生了变化。即使输入相同的nums,多次调用的结果也会不同(第一次返回 6,第二次返回 9),因为数组被持续修改。
3.为什么 React 强调纯函数?
React 组件的本质是 "输入 props,输出 JSX" 的函数。React 强调纯函数,是因为纯函数带来了可预测性 和可维护性:
- 可预测性:相同的 props 渲染出相同的 UI,方便调试和测试。
- 可维护性:纯函数组件逻辑清晰,不依赖外部状态,更易理解和修改。
但实际开发中,组件不可能完全是纯函数(比如需要请求数据、设置定时器),因此 React 用useEffect来专门处理副作用,让组件主体保持纯函数特性,副作用逻辑单独管理。
三、核心常用 Hooks(日常开发高频使用)
React 提供了多个内置 Hooks,其中useState和useEffect是日常开发中使用频率最高的两个,堪称 "Hooks 双子星"。
1. useState - 管理组件内部状态
(1)什么是状态?
状态(State)是组件内部管理的、可以变化的数据。当状态变化时,组件会重新渲染,UI 也会随之更新。比如计数器的数值、表单的输入内容、弹窗的显示 / 隐藏状态等,都是常见的状态。
(2)用途
useState的作用是在函数组件中创建和管理状态,让组件拥有 "记忆能力"------ 即使组件重新渲染,状态也能被保留。
(3)核心规则
- 只能在函数组件或自定义 Hooks 的顶层调用(不能在 if、for 等条件 / 循环语句中使用)。
- 每次组件渲染时,
useState都会返回当前的状态和更新函数。
(4)代码示例
jsx
// useState示例
import { useState } from 'react';
export default function App() {
// 方式1:直接传入初始值(简单场景)
// const [num, setNum] = useState(1);
// 方式2:函数式初始化(复杂场景,初始值需要计算时使用)
const [num, setNum] = useState(() => {
// 函数体只会在组件首次渲染时执行(性能优化)
const num1 = 1 + 2;
const num2 = 2 + 3;
return num1 + num2; // 初始值为6
});
return (
// 点击时更新状态:使用函数式更新(依赖上一次状态时推荐)
<div onClick={() => setNum(prevNum => {
console.log(prevNum); // 打印上一次的num值
return prevNum + 1;
})}>
{num} {/* 渲染当前状态 */}
</div>
)
}
效果亮个相:

代码解析:
const [num, setNum] = useState(initialValue):通过数组解构获取状态(num)和更新函数(setNum)。- 函数式初始化:当初始值需要复杂计算时,传入一个函数,该函数的返回值作为初始值,且只会在组件首次渲染时执行,避免重复计算。
- 函数式更新:
setNum(prevNum => prevNum + 1)确保使用的是 "上一次的状态",在连续更新状态时更可靠(比如多次点击计数器)。
2. useEffect - 处理副作用
(1)用途
useEffect专门用于处理组件中的副作用,比如:
- 发送网络请求获取数据
- 设置 / 清除定时器、计时器
- 注册 / 移除事件监听
- 操作 DOM
它相当于 Class 组件中componentDidMount(挂载)、componentDidUpdate(更新)、componentWillUnmount(卸载)三个生命周期函数的组合。
(2)规则
- 同
useState,只能在函数组件或自定义 Hooks 的顶层调用。 - 可以返回一个清理函数,用于清除副作用(如清除定时器、移除事件监听)。
(3)代码示例
jsx
// useEffect示例
import { useEffect } from 'react';
export default function Demo() {
useEffect(() => {
// 组件挂载时执行(类似componentDidMount)
console.log('组件挂载了');
// 副作用:设置定时器
const timer = setInterval(() => {
console.log('定时器执行中...');
}, 1000);
// 清理函数:组件卸载时执行(类似componentWillUnmount)
return () => {
console.log('组件卸载了');
clearInterval(timer); // 清除定时器,避免内存泄漏
}
}, []); // 依赖数组为空
return <div>副作用示例</div>;
}
效果亮个相:

代码解析:
useEffect的第一个参数是一个回调函数,包含要执行的副作用逻辑。- 回调函数可以返回一个清理函数,在组件卸载或副作用重新执行前调用,用于 "收拾残局"(如清除定时器,避免内存泄漏)。
- 第二个参数是依赖数组(下文详细讲解),这里为空数组表示 "只在组件挂载时执行一次,卸载时执行清理函数"。
(4)没有清除定时器,内存泄漏的情况举例
import { useState, useEffect } from 'react';
// 被控制挂载/卸载的Demo组件(注释清理定时器代码,模拟泄漏)
function Demo() {
useEffect(() => {
// 组件挂载时执行
console.log('Demo组件挂载了');
// 副作用:设置每秒打印的定时器
const timer = setInterval(() => {
console.log('定时器执行中...(内存泄漏中)', new Date().toLocaleTimeString());
}, 1000);
// 【关键】注释清理函数,模拟"不清除定时器"的场景
// return () => {
// console.log('Demo组件卸载了');
// clearInterval(timer); // 注释掉这行,不清理定时器
// }
}, []); // 依赖数组为空
return <div>副作用示例(定时器未清理)</div>;
}
// 父组件:通过开关控制Demo组件的挂载/卸载
export default function App() {
// 控制Demo组件是否显示的开关
const [showDemo, setShowDemo] = useState(true);
return (
<div style={{ padding: '20px' }}>
{/* 开关按钮:控制Demo组件挂载/卸载 */}
<button
onClick={() => setShowDemo(!showDemo)}
style={{ padding: '8px 16px', marginBottom: '20px' }}
>
{showDemo ? '卸载Demo组件' : '挂载Demo组件'}
</button>
{/* 条件渲染:showDemo为true则挂载,false则卸载 */}
{showDemo && <Demo />}
</div>
);
}
效果亮个相:

-
初始状态:
Demo组件挂载,控制台会打印:Demo组件挂载了 定时器执行中...(内存泄漏中) 10:00:00 定时器执行中...(内存泄漏中) 10:00:01 定时器执行中...(内存泄漏中) 10:00:02(每秒打印一次,符合预期)
-
点击 "卸载 Demo 组件" 按钮
- 现象 1:页面上的
Demo组件消失(已卸载); - 现象 2:控制台仍然每秒打印定时器日志 !即使组件已经卸载,定时器还在后台运行,这就是内存泄漏------ 组件已销毁,但它创建的定时器还占用浏览器内存和 CPU 资源,直到页面刷新。
- 现象 1:页面上的
-
多次点击 "挂载 / 卸载" 按钮
- 现象:每挂载一次
Demo组件,就会新增一个定时器;卸载后所有定时器都不会停止,控制台打印日志的频率会越来越快(比如挂载 2 次后,每秒打印 2 条日志),内存占用持续升高。
- 现象:每挂载一次
只要在 useEffect 中创建了定时器、事件监听等 "外部资源",必须在清理函数中销毁,不然可能出现资源叠加、内存泄漏,这是 React 开发的核心避坑点。
四、useEffect 接收的参数 --- 依赖数组
useEffect的灵活性很大程度上来自它的第二个参数 ------ 依赖数组。理解依赖数组是掌握useEffect的关键。
1.useEffect 接收哪些参数?
useEffect接收两个参数:
javascript
useEffect(effectCallback, dependencies);
- 第一个参数
effectCallback:副作用逻辑的函数。 - 第二个参数
dependencies:依赖数组,一个包含变量的数组(可选)。
2.什么是依赖数组?作用是什么?
依赖数组是useEffect的 "触发器",它决定了effectCallback何时执行。React 会监听数组中变量的变化,当变量发生变化时,会先执行上一次的清理函数,再执行新的effectCallback。
简单来说:依赖数组中的变量,就是副作用逻辑 "依赖" 的外部状态。当这些状态变化时,副作用需要重新执行。
3.依赖项的三种情况
(1) 依赖项为空数组 []
-
执行时机 :仅在组件挂载时执行一次
effectCallback,组件卸载时执行清理函数。 -
适用场景:只需要初始化一次的操作,如获取初始化数据、设置全局事件监听(如滚动监听)。
-
代码示例:
jsx
// Demo.jsx中依赖为空数组的场景 useEffect(() => { console.log('组件挂载时执行(只一次)'); const timer = setInterval(() => { console.log('定时器运行中'); }, 1000); return () => { console.log('组件卸载时执行'); clearInterval(timer); // 清理定时器 } }, []); // 空数组:不依赖任何状态 -
避坑要点:
- 不要在回调函数中使用组件内的状态或 props,因为闭包会导致获取到的是初始值(不会随状态更新而变化)。
- 必须在清理函数中清除所有可能导致内存泄漏的操作(如定时器、事件监听)。
(2) 依赖项为有值数组 [a, b, c]
-
执行时机 :组件挂载时执行一次,之后每当
a、b、c中任意一个值发生变化时,先执行上一次的清理函数,再执行新的effectCallback。 -
适用场景:副作用逻辑依赖特定状态,当状态变化时需要重新执行,如根据 ID 获取数据、根据输入值过滤列表。
-
代码示例:
jsx
// App.jsx中依赖num的场景 useEffect(() => { console.log('num变化了,重新执行副作用'); // 依赖num的定时器:每次num变化时,定时器打印最新的num const timer = setInterval(() => { console.log('当前num:', num); }, 1000); return () => { console.log('清理上一次的定时器'); clearInterval(timer); // 避免多个定时器同时运行 } }, [num]); // 依赖num:num变化时触发 -
避坑要点:
- 必须将所有在
effectCallback中使用的状态、props、函数都加入依赖数组,否则可能获取到旧值(闭包问题)。 - 避免依赖引用类型(如对象、数组),因为 React 采用 "浅比较",可能导致不必要的重复执行(可通过
useMemo优化)。
- 必须将所有在
(3) 无依赖项(省略第二个参数)
-
执行时机 :组件挂载时执行一次,之后每次组件重新渲染时 都会先执行上一次的清理函数,再执行新的
effectCallback。 -
适用场景:极少使用,一般用于需要 "每次渲染都执行" 的操作(如同步 DOM 到状态)。
-
代码示例:
jsx
// App.jsx中无依赖项的场景 useEffect(() => { console.log('每次渲染都会执行'); // 每次渲染都打印当前num(包括初始渲染和所有更新) console.log('当前num:', num); }); // 无依赖数组:每次渲染都触发 -
避坑要点:
- 性能隐患:频繁渲染会导致副作用频繁执行,可能引发性能问题(如多次发送请求)。
- 清理函数会在 "每次重新渲染前" 执行,需确保清理逻辑轻量。
4.三种依赖项对比
| 依赖项形式 | 执行时机 | 典型场景 | 性能影响 |
|---|---|---|---|
[] |
仅挂载 / 卸载 | 初始化操作 | 性能最优(只执行一次) |
[a, b] |
挂载 + 依赖变化时 | 依赖特定状态的副作用 | 性能中等(依赖变化时执行) |
| 无依赖 | 每次渲染 | 极少场景 | 性能最差(频繁执行) |
5.依赖项的 "浅比较" 规则
React 对依赖数组的变化检测采用 "浅比较":
- 基本类型(number/string/boolean/null/undefined):比较值是否相等(如
1 === 1、'a' === 'a')。 - 引用类型(object/array/function):比较引用地址是否相同(如
{a:1}与{a:1}因地址不同被视为变化)。
示例:
jsx
// 错误示例:每次渲染都会创建新对象,导致useEffect频繁执行
useEffect(() => {
console.log('依赖对象变化了');
}, [{ name: 'react' }]); // 每次渲染都会生成新对象,视为依赖变化
// 正确示例:用useMemo缓存对象,避免不必要的更新
const user = useMemo(() => ({ name: 'react' }), []);
useEffect(() => {
console.log('依赖对象稳定了');
}, [user]); // 依赖缓存后的对象,仅初始化时执行
五、面试官会问
- 为什么 Hooks 只能在函数组件的顶层调用? 答:React 通过 Hooks 的调用顺序来识别和管理状态。如果在条件语句(如
if)或循环中调用 Hooks,会导致每次渲染时 Hooks 的调用顺序不一致,React 无法正确关联状态和组件,从而引发错误。 - useState 的初始值函数什么时候执行? 答:当初始值需要通过复杂计算得到时,
useState( () => initialValue )中的函数仅在组件首次渲染时执行一次,后续渲染会忽略。这是一种性能优化,避免重复计算。 - useEffect 的清理函数什么时候执行? 答:有两种情况:① 组件卸载时;② 依赖数组中的变量变化,重新执行
effectCallback之前。清理函数的作用是清除上一次副作用产生的 "遗留影响"(如定时器、事件监听),避免内存泄漏。 - 如何解决 useEffect 中的闭包问题? 答:闭包问题指
effectCallback中获取到的状态是旧值(因为回调函数在创建时捕获了当时的状态)。解决方法:① 将依赖的状态加入依赖数组;② 用useRef存储最新状态(ref.current可获取实时值)。 - useState 和 useReducer 的区别? 答:
useState适用于简单状态管理(如单个数值、布尔值);useReducer适用于复杂状态逻辑(如状态之间相互依赖、多个子值组成的状态),通过 "action" 来描述状态变化,更易预测和调试。 - 为什么依赖数组中加入函数时要小心? 答:函数在每次组件渲染时可能被重新创建(引用变化),导致
useEffect误判为依赖变化而频繁执行。解决方法:① 用useCallback缓存函数;② 将函数定义在effectCallback内部(如果仅在副作用中使用)。
六、结语
React Hooks 的出现,不仅简化了组件的写法,更重塑了我们对 React 组件逻辑的组织方式。useState让函数组件拥有了状态管理能力,useEffect让副作用逻辑变得可控可预测,而依赖数组的灵活运用则让副作用的执行时机尽在掌握。
掌握 Hooks 的核心,在于理解 "纯函数" 与 "副作用" 的分离思想 ------ 让组件主体保持纯净,让副作用逻辑可管可控。无论是日常开发中的计数器、表单处理,还是复杂的状态管理、异步请求,Hooks 都能以简洁的代码实现高效的逻辑。
希望通过本文,你能对 React Hooks 有更清晰的认识。在实际开发中,多写多练,结合具体场景灵活运用useState和useEffect,你会发现函数组件的真正魅力!