🎯 React函数组件的"生活管家"------useEffect Hook详解
1. 🌟 开篇:从生活中的"副作用"说起
嘿,各位掘友们!今天咱们来聊聊React函数组件里的一个"大管家"------useEffect
Hook。你可能会问,这玩意儿是干啥的?别急,咱们先从生活中的"副作用"聊起。
想想看,你是不是有过这样的经历:感冒了吃药,药效是把感冒治好,但可能伴随着"犯困"的副作用;或者,为了项目熬夜加班,项目是上线了,但黑眼圈和掉发也成了"副作用"。这些"副作用"虽然不是我们主要的目的,但它们确实在我们的"主线任务"完成后,或者在任务进行中,悄悄地发生了。
在React的世界里,也有类似的"副作用"。比如,我们更新了UI(主线任务),但可能需要同时做一些与UI渲染本身无关的事情,比如:
- 数据获取:组件渲染后,需要从服务器拉取数据。
- 订阅事件:组件挂载后,需要监听一些全局事件(比如鼠标移动、键盘按下)。
- 手动修改DOM:虽然React鼓励我们声明式地操作DOM,但有时候我们可能需要直接操作DOM(比如集成第三方库)。
- 定时器:设置一个定时器来更新组件状态。
这些操作,就是React组件的"副作用"。在类组件中,我们有componentDidMount
、componentDidUpdate
和componentWillUnmount
这些生命周期方法来处理这些副作用。但函数组件呢?它没有这些生命周期方法啊!难道函数组件就不能有"副作用"了吗?
当然不是!useEffect
就是React为函数组件量身打造的"副作用管家",它能让我们在函数组件中也能优雅地处理各种副作用,让函数组件也能像类组件一样,拥有完整的"生命周期"体验。是不是很神奇?接下来,咱们就一起揭开useEffect
的神秘面纱!

2. 🔄 React生命周期与useEffect的关系
在深入了解useEffect
之前,我们先来快速回顾一下类组件的生命周期。如果你是React的老兵,这部分可以快速跳过;如果你是新手,这部分能帮你更好地理解useEffect
的强大之处。
类组件的"一生"
类组件的生命周期就像一个人从出生、成长到离开的过程,主要分为三个阶段:
- 挂载阶段(Mounting) :组件被创建并插入到DOM中。这个阶段会依次调用
constructor
、static getDerivedStateFromProps
、render
和componentDidMount
。 - 更新阶段(Updating) :组件的props或state发生变化时,组件会重新渲染。这个阶段会依次调用
static getDerivedStateFromProps
、shouldComponentUpdate
、render
、getSnapshotBeforeUpdate
和componentDidUpdate
。 - 卸载阶段(Unmounting) :组件从DOM中移除。这个阶段会调用
componentWillUnmount
。
下图展示了类组件的生命周期流程:

useEffect
:函数组件的"生命周期模拟器"
函数组件本身没有这些生命周期方法,但useEffect
的出现,让函数组件也能拥有类似生命周期的能力。它就像一个多面手,能够根据你的需求,扮演componentDidMount
、componentDidUpdate
和componentWillUnmount
的角色。
1. 挂载时执行:componentDidMount
的替代者
当你希望在组件首次渲染(挂载)后执行一些操作时,比如数据请求、事件监听等,useEffect
可以完美替代componentDidMount
。你只需要给useEffect
的第二个参数传入一个空数组[]
,它就会在组件挂载后执行一次,之后就不会再执行了。
2. 更新时执行:componentDidUpdate
的替代者
当组件的某些状态或属性发生变化时,你希望执行一些操作,比如根据新的数据重新计算、更新DOM等,useEffect
可以替代componentDidUpdate
。你只需要把需要监听的状态或属性放到useEffect
的第二个参数(依赖数组)中,当这些依赖项发生变化时,useEffect
就会重新执行。
3. 卸载时执行:componentWillUnmount
的替代者
当组件即将从DOM中移除时,你可能需要做一些清理工作,比如清除定时器、取消事件监听等,以防止内存泄漏。useEffect
的回调函数可以返回一个清理函数,这个清理函数会在组件卸载时执行。这就像componentWillUnmount
的作用。
是不是感觉useEffect
很强大?它把类组件中分散在不同生命周期方法里的副作用逻辑,都集中到了一个API里,让我们的代码更加简洁和易于维护。接下来,咱们就来看看useEffect
的具体用法。

3. ⚡ useEffect基础语法详解
useEffect
的语法非常简洁,就像它的名字一样,就是"使用效果":
javascript
useEffect(() => {
// 在这里执行副作用操作
return () => {
// 在这里执行清理操作
};
}, [依赖项]);
它接收两个参数:
第一个参数:回调函数(callback
)
这个回调函数就是你放置副作用逻辑的地方。当组件渲染完成后,React会执行这个函数。你可以在这里进行数据请求、DOM操作、事件监听等任何你需要的副作用操作。
小贴士:这个回调函数是同步执行的,但它会在浏览器完成布局和绘制之后,在一个单独的"副作用阶段"执行,所以它不会阻塞浏览器渲染。
第二个参数:依赖数组(array
,可选)
这是一个可选的数组,用于控制useEffect
的执行时机。它就像useEffect
的"开关"和"过滤器":
- 如果你不提供这个参数 :
useEffect
会在每次组件渲染后都执行。这就像一个"话痨",组件一有风吹草动,它就出来"唠叨"一番。 - 如果你提供一个空数组
[]
:useEffect
只会在组件首次挂载时执行一次,之后无论组件如何更新,它都不会再执行。这就像一个"专一"的管家,只在主人"入住"时忙活一次,之后就"退休"了。 - 如果你提供一个包含依赖项的数组
[dep1, dep2, ...]
:useEffect
会在组件首次挂载时执行一次,并且在数组中的任何一个依赖项发生变化时,它会重新执行。这就像一个"敏感"的管家,只对它"关心"的事情做出反应。
返回值:清理函数(cleanup function
)
useEffect
的回调函数可以返回一个函数,这个返回的函数就是"清理函数"。它的作用是在下一次useEffect
执行之前,或者在组件卸载之前,执行一些清理工作。比如,清除定时器、取消事件监听、取消网络请求等。
为什么需要清理函数?
想象一下,你打开了一个水龙头(设置了定时器),如果用完不关(不清除定时器),水就会一直流,造成浪费(内存泄漏)。清理函数就是那个帮你"关水龙头"的。它能确保你的副作用操作不会留下"烂摊子",避免不必要的资源占用和潜在的bug。
清理函数会在以下两种情况下执行:
- 组件卸载时:当组件从DOM中移除时,清理函数会执行。
- 依赖项变化时 :在
useEffect
重新执行之前(因为依赖项发生了变化),上一次的清理函数会先执行,然后再执行新的副作用函数。
理解了这些基础概念,我们就可以开始探索useEffect
的"三种人格"了!
4. 🎭 useEffect的三种"人格"
useEffect
就像一个拥有多重人格的"演员",它会根据你给它的"剧本"(依赖数组)来决定如何"表演"。
话痨模式:无依赖数组
javascript
useEffect(() => {
console.log('我每次渲染都会执行!');
});
当你不给useEffect
提供第二个参数(依赖数组)时,它就会进入"话痨模式"。这意味着,组件的每一次渲染,它都会执行一次 。无论是组件首次挂载,还是组件的props或state发生任何变化导致重新渲染,这个useEffect
都会被触发。
适用场景:这种模式在实际开发中比较少用,因为它可能会导致不必要的性能开销。但如果你确实需要在每次渲染后都执行某些操作,比如每次渲染后都记录日志,或者每次渲染后都进行一些DOM操作,那么这种模式是适用的。
专一模式:空依赖数组 []
javascript
useEffect(() => {
console.log('我只在组件首次挂载时执行一次!');
// 比如:数据请求、事件监听
return () => {
console.log('我只在组件卸载时执行一次!');
// 比如:清除事件监听
};
}, []);
当你给useEffect
提供一个空数组[]
作为第二个参数时,它就进入了"专一模式"。这意味着,它只会在组件首次挂载时执行一次。之后无论组件如何更新,它都不会再执行。它的清理函数也只会在组件卸载时执行一次。
适用场景 :这种模式非常常用,它完美替代了类组件的componentDidMount
和componentWillUnmount
。例如:
- 初始化数据请求:在组件加载完成后,只请求一次数据。
- 添加全局事件监听:比如监听窗口大小变化、键盘事件等,并在组件卸载时移除监听。
- 初始化第三方库:在组件挂载时初始化一些只需要执行一次的第三方库。
敏感模式:有依赖数组 [dep1, dep2, ...]
javascript
useEffect(() => {
console.log('我的依赖项变化了,我重新执行了!');
// 比如:根据count的变化更新标题
return () => {
console.log('我的依赖项变化了,我先清理一下上一次的副作用!');
};
}, [count]); // 只有当count变化时才执行
当你给useEffect
提供一个包含依赖项的数组时,它就进入了"敏感模式"。这意味着,它会在组件首次挂载时执行一次,并且只有当数组中的任何一个依赖项发生变化时,它才会重新执行。在重新执行之前,它会先执行上一次的清理函数。
适用场景 :这种模式是useEffect
最强大和最常用的模式,它替代了类组件的componentDidUpdate
。例如:
- 根据props或state的变化请求数据:当用户ID变化时,重新请求用户数据。
- 根据输入框内容变化进行搜索:当搜索关键词变化时,重新发起搜索请求。
- 动态修改DOM:根据某个状态的变化来修改DOM元素的样式或属性。
理解了这三种"人格",你就能更好地驾驭useEffect
,让它在你的React应用中发挥最大的作用。接下来,咱们就通过一个实战案例,来感受一下useEffect
的魅力!
5. 🛠️ 实战案例:计数器小应用
理论知识讲了这么多,是时候来点实际的了!咱们用一个简单的计数器应用,来感受一下useEffect
的强大。
这个计数器应用有以下几个功能:
- 显示当前的计数。
- 点击"增加"按钮,计数加1。
- 点击"改变"按钮,改变一个名称。
- 组件挂载后,每秒自动增加计数。
- 组件卸载时,清除定时器,防止内存泄漏。
javascript
import React, { useEffect, useState } from 'react';
// import { root } from "./main"; // 如果是实际项目,可能需要引入ReactDOM的unmount方法
function App() {
// 使用useState定义计数器状态count和更新函数setCount
const [count, setCount] = useState(0);
// 使用useState定义名称状态name和更新函数setName
const [name, setName] = useState("小滴课堂");
// 增加计数的方法
const add = () => {
setCount(prevCount => prevCount + 1); // 使用函数式更新,确保获取到最新的count值
};
// 改变名称的方法
const change = () => {
setName("xdclass.net");
};
// 卸载组件的方法(这里只是模拟,实际应用中通常由路由或父组件控制)
const handleDelet = () => {
// root.unmount(); // 实际项目中,如果需要卸载整个React应用,可以使用ReactDOM.unmount
console.log("模拟组件卸载");
};
// 使用useEffect处理副作用:设置定时器和清理定时器
useEffect(() => {
// 设置一个定时器,每秒更新一次count
const timer = setInterval(() => {
setCount(prevCount => prevCount + 1); // 使用函数式更新,避免闭包陷阱,确保每次都基于最新状态更新
}, 1000);
// 返回一个清理函数,在组件卸载或依赖项变化前执行
return () => {
clearInterval(timer); // 清除定时器,防止内存泄漏
console.log("组件卸载了,定时器已清除!");
};
}, []); // 空数组表示这个useEffect只在组件挂载和卸载时执行一次
// 另一个useEffect,用于在count变化时更新页面标题
useEffect(() => {
document.title = `你点击了 ${count} 次!`;
console.log(`页面标题更新为: 你点击了 ${count} 次!`);
}, [count]); // 依赖项为count,只有当count变化时才执行
return (
<div>
<h1>当前的计数: {count}</h1>
<button onClick={add}>增加</button>
<h1>{name}</h1>
<button onClick={change}>改变</button>
<button onClick={handleDelet}>卸载组件</button>
</div>
);
}
export default App;
🖼️ 效果演示:
代码解析:
useState(0)
和useState("小滴课堂")
:我们使用useState
Hook来定义两个状态变量:count
(计数器)和name
(名称)。它们分别有初始值0和"小滴课堂"。add
和change
函数 :这两个是普通的JavaScript函数,用于更新count
和name
状态。注意setCount(prevCount => prevCount + 1)
这种函数式更新的方式,它能确保在异步更新时,总是基于最新的状态值进行计算,避免闭包陷阱。- 第一个
useEffect
:- 它接收一个空数组
[]
作为依赖项,这意味着它只会在组件首次挂载时执行一次。这完美模拟了componentDidMount
的行为。 - 在回调函数中,我们使用
setInterval
设置了一个定时器,每秒钟让count
加1。这里同样使用了函数式更新setCount(prevCount => prevCount + 1)
。 - 它返回了一个清理函数
return () => { clearInterval(timer); ... }
。这个清理函数会在组件卸载时执行,负责清除定时器。这完美模拟了componentWillUnmount
的行为,防止了内存泄漏。
- 它接收一个空数组
- 第二个
useEffect
:- 它接收
[count]
作为依赖项,这意味着只有当count
的值发生变化时,这个useEffect
才会重新执行。这完美模拟了componentDidUpdate
的行为。 - 在回调函数中,我们修改了页面的标题,使其显示当前的计数。
- 它接收
通过这个例子,你可以清晰地看到useEffect
是如何在函数组件中处理挂载、更新和卸载阶段的副作用的。它把这些逻辑集中在一起,让代码更加清晰和易于管理。

6. ⚠️ 常见陷阱与最佳实践
在使用useEffect
的过程中,有一些常见的陷阱需要避免,同时也有一些最佳实践可以让你的代码更加健壮和高效。
陷阱一:无限循环的噩梦
这是新手最容易踩的坑!看看下面这个例子:
javascript
// ❌ 错误示例:会导致无限循环
function BadExample() {
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
useEffect(() => {
// 每次渲染都会执行,导致无限循环
setUser({ name: 'John', age: count });
}); // 注意:这里没有依赖数组!
return <div>Count: {count}</div>;
}
问题分析 :由于没有提供依赖数组,useEffect
会在每次渲染后执行。而setUser
会触发组件重新渲染,重新渲染又会触发useEffect
,形成无限循环。
正确做法:
javascript
// ✅ 正确示例:使用依赖数组控制执行时机
function GoodExample() {
const [count, setCount] = useState(0);
const [user, setUser] = useState(null);
useEffect(() => {
// 只有当count变化时才执行
setUser({ name: 'John', age: count });
}, [count]); // 明确指定依赖项
return <div>Count: {count}</div>;
}
陷阱二:依赖数组的"遗漏症"
另一个常见问题是在依赖数组中遗漏了某些依赖项:
javascript
// ❌ 错误示例:遗漏了依赖项
function BadExample() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
useEffect(() => {
const result = count * multiplier;
console.log(`结果: ${result}`);
}, [count]); // 遗漏了multiplier!
return (
<div>
<p>Count: {count}</p>
<p>Multiplier: {multiplier}</p>
</div>
);
}
问题分析 :当multiplier
变化时,useEffect
不会重新执行,因为依赖数组中没有包含multiplier
。这可能导致显示的结果不正确。
正确做法:
javascript
// ✅ 正确示例:包含所有依赖项
function GoodExample() {
const [count, setCount] = useState(0);
const [multiplier, setMultiplier] = useState(2);
useEffect(() => {
const result = count * multiplier;
console.log(`结果: ${result}`);
}, [count, multiplier]); // 包含所有使用到的状态变量
return (
<div>
<p>Count: {count}</p>
<p>Multiplier: {multiplier}</p>
</div>
);
}
陷阱三:忘记清理的"内存泄漏"
这是一个非常严重的问题,可能导致内存泄漏和性能问题:
javascript
// ❌ 错误示例:忘记清理定时器
function BadExample() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 忘记返回清理函数!
}, []);
return <div>Count: {count}</div>;
}
问题分析:当组件卸载时,定时器仍然在运行,这会导致内存泄漏,并且可能在组件已经卸载后还尝试更新状态,导致警告或错误。
正确做法:
javascript
// ✅ 正确示例:记得清理副作用
function GoodExample() {
const [count, setCount] = useState(0);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
// 返回清理函数
return () => {
clearInterval(timer);
};
}, []);
return <div>Count: {count}</div>;
}
最佳实践
1. 使用ESLint插件
安装eslint-plugin-react-hooks
插件,它能帮你自动检测useEffect
的依赖项问题:
bash
npm install eslint-plugin-react-hooks --save-dev
2. 一个useEffect
只做一件事
不要在一个useEffect
中处理多个不相关的副作用,这样会让代码难以理解和维护:
javascript
// ❌ 不推荐:一个useEffect处理多个不相关的事情
useEffect(() => {
// 处理数据请求
fetchUserData();
// 处理定时器
const timer = setInterval(() => {
updateTime();
}, 1000);
// 处理事件监听
window.addEventListener('resize', handleResize);
return () => {
clearInterval(timer);
window.removeEventListener('resize', handleResize);
};
}, []);
// ✅ 推荐:分离不同的副作用
useEffect(() => {
fetchUserData();
}, []);
useEffect(() => {
const timer = setInterval(() => {
updateTime();
}, 1000);
return () => clearInterval(timer);
}, []);
useEffect(() => {
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
3. 使用自定义Hook封装复杂逻辑
当useEffect
的逻辑变得复杂时,考虑将其封装成自定义Hook:
javascript
// 自定义Hook:useTimer
function useTimer(initialCount = 0) {
const [count, setCount] = useState(initialCount);
useEffect(() => {
const timer = setInterval(() => {
setCount(prev => prev + 1);
}, 1000);
return () => clearInterval(timer);
}, []);
return count;
}
// 在组件中使用
function MyComponent() {
const count = useTimer(0);
return <div>Timer: {count}</div>;
}
记住这些陷阱和最佳实践,你就能更好地驾驭useEffect
,写出更加健壮和高效的React代码!
7. 🎉 总结:useEffect让函数组件"活"起来
恭喜你,已经掌握了useEffect
这个React函数组件的"生活管家"!
通过今天的学习,我们了解到:
- 副作用无处不在:在React应用中,数据请求、DOM操作、事件监听等都是常见的副作用。
useEffect
是函数组件处理副作用的利器:它能够模拟类组件的生命周期方法,让函数组件也能优雅地处理挂载、更新和卸载阶段的逻辑。- 依赖数组是
useEffect
的"灵魂" :通过控制依赖数组,我们可以精确地控制useEffect
的执行时机,实现"话痨模式"、"专一模式"和"敏感模式"。 - 清理函数至关重要:它能帮助我们避免内存泄漏,确保副作用操作的"善始善终"。
- 避免常见陷阱,遵循最佳实践:正确使用依赖数组,分离副作用逻辑,并善用自定义Hook,能让你的代码更加健壮和易于维护。
useEffect
的出现,极大地提升了React函数组件的能力,让我们可以用更简洁、更声明式的方式来编写组件逻辑。它让函数组件不再是简单的UI渲染器,而是能够拥有完整"生命"的、充满活力的"个体"。
希望这篇博客能帮助你更好地理解和使用useEffect
。如果你有任何疑问或心得,欢迎在评论区交流!我们下期再见!