useEffect 不可忽视的 cleanup 函数
在 react 开发中, useEffect 是我们经常会使用到的钩子,一个基础的例子如下:
useEffect(() => {// some code here// cleanup 函数return () => {doSomething()}
}, [dependencies])
上述代码中, cleanup 函数的执行时机有如下两种:
- 组件卸载时会执行一次
- 每个依赖项(dependencies)变更后,页面重新渲染前会执行一次
在日常的开发中,我们很多人都会忽略掉 cleanup 函数,在一般情况下,不写 cleanup 函数不会有什么问题。但如果我们在 useEffect中使用了定时器或者进行了网络请求,这种情况下,如果不在 cleanup 函数中执行一些清除逻辑,会导致一些潜在的 bug。下面来聊聊如何使用 cleanup 函数解决内存占用和网络请求竞态问题.
不在 cleanup 函数中清除旧定时器会发生什么?
我们使用 interval 写一个计数器例子,count 从 0 开始,每秒递增 1,如下:
function Counter() {let [count, setCount] = useState(0)useEffect(() => {console.log('effect')setInterval(() => {setCount(count + 1)}, 1000)}, [count])return (<div><div>{count}</div></div>)
}
上述例子,一开始递增到前面几个数字,页面看起来是正常的,但是越往后,就会发现,数字在闪烁,而且之后页面会卡住,一段时间后的结果如下:

上图中,count 递增到 15 ,但 useEffect 却运行了 95 次,也就是,此时页面上已经有了 95 个定时器,越往后,定时器越多,为什么会这样?正常的结果应该是 count 增加到多少, useEffect 就运行多少次 (除了初始渲染运行的那一次),因为我们把 count 作为 useEffect 的依赖项。
导致上述问题的根本原因是我们没有清除旧的定时器,count 发生变化时,组件会重新渲染, useEffect 重新运行,会创建一个新的定时器,而旧的定时器并没有被清除,导致多个定时器同时存在,占用了更多的内存,这就是后续页面卡住的原因。
而页面上的数字闪烁的问题是因为每次组件重新渲染时,都会重新调用 useEffect,导致新的定时器开始运行,而旧的定时器还在继续工作。这样就会出现两个定时器交替执行的情况,导致数字的变化不稳定,造成闪烁的效果。
解决办法就是在 cleanup 函数中清除旧的定时器,代码如下:
function Counter() {// ...useEffect(() => {console.log('effect')const interval = setInterval(() => {setCount(count + 1)}, 1000)return () => {clearInterval(interval)}}, [count])// ...
}
清除旧的定时器之后,页面的表现就符合预期了,每次重新渲染,页面上有且只有一个定时器。
除了定时器这种场景,其他场景,如订阅、使用 IntersectionObserver 观察者,都需要注意在 cleanup 函数中执行对应的清除逻辑,以避免内存泄露问题。
cleanup 函数解决网络请求问题
case1:离开页面时请求未完成
一个在 useEffect 中进行网络请求例子如下:
function List() {const [lists,setLists] = useState([])useEffect(() => {fetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()).then(data => {alert('请求完成')setLists(data)})}, [])return (<div>{lists?.map((item) => (<p key={item.id}>{item.title}</p>))}</div>)
}export default List
上述代码是一个列表组件,通过 fetch 请求获取列表数据,最终渲染在页面上,这看起来没什么问题,我们平常就是这么写的。
我们增加一个 Home 组件,功能很简单,就是从 Home 页面跳转到上述的 List 页面,代码如下:
import { Link } from 'react-router-dom'function Home() {return (<div><Link to="/list">Go to lists</Link></div>)
}export default Home
我们从 Home 页面跳转到 List 页面,然后 List 页面开始请求 list 列表数据。跳到 List 页面后,在请求完成之前通过浏览器的回退按钮回到 Home 页面,交互过程如下:
浏览器后退按钮取消请求
视频中,我们把网络设置成弱网环境,在 List 页面的 fetch 请求还没完成,就回到了 Home 页面,我们期望 then 回调里的代码不应该执行,因为离开了 List 页面后,List 组件相当于销毁了,不应该再继续执行组件代码,然而结果却是执行了。
造成上述结果的原因是:fetch 请求是异步的,进入 List 页面请时求已经发出,离开 List 页面没有取消请求,所以请求继续进行,完成后就会执行 then 回调里的代码。
解决办法如下:
// ...
useEffect(() => {let isCancelled = falsefetch('https://jsonplaceholder.typicode.com/todos').then(res => res.json()).then(data => {if (!isCancelled) {alert('请求已完成')setLists(data)}})return () => {isCancelled = true}
}, [])
// ...
上述代码中,我们定义了一个标志位变量:isCancelled,表示当前请求是否需要取消(这里并不是真正的取消请求,如何取消请求下文会讲到),初始化为 false,在 then 回调里,只有判断请求不需要取消才会执行后续代码。最后,重点来了,前面讲到 cleanup 函数的执行时机之一是在组件销毁时,所以在离开 List 页面时,我们把 isCancelled 设置为 true,那么后续请求完成时,就不会进入 if (!isCancelled) {} 条件语句中。
case2:同时触发多个相同请求,网络竞态问题
定义一个 User 组件,如下:
import { useState, useEffect } from "react";
import {useLocation,Link} from 'react-router-dom'function User() {const [curUser,setCurUser] = useState({})const route = useLocation()const id = route.pathname.split('/')[2]useEffect(() => {fetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(res => res.json()).then(data => {setCurUser(data)})}, [id])return (<div style={{display: 'flex',flexDirection: 'column',alignItems: 'center'}}><p>name: {curUser.name}</p><p>username: {curUser.username}</p><p>email: {curUser.email}</p><Link to='/users/1'>fetch user 1</Link><Link to='/users/2'>fetch user 2</Link><Link to='/users/3'>fetch user 3</Link></div>)
}export default User
上述是一个获取用户信息并展示在页面上的例子,我们点击 Link 组件,更新路由中的用户 id,在 useEffect 中根据 id 去请求用户数据。在网络正常的情况下,我们快速切换用户 id,页面上的用户信息就会跟着改变,这看起来没什么问题。如果我们把网络设置成弱网环境,同样的操作结果如下:
网络请求竞态视频1
在上述视频中,当前用户是 user1,我们快速点击 fetch user2、fetch user3,并最终停留在 user3,正常情况下,最终页面展示的应该是 user3 的信息,但是实际结果是:先展示 user2 的信息,然后再展示 user3 的信息。
上述问题其实涉及到了网络竞态问题。网络竞态是:用户触发同一个请求多次,由于网络的波动,每个请求的响应时间是不一样的,最先触发的请求可能是最后一个返回响应的,最后触发的请求也可能是最先返回响应的,最终所有请求完成后,我们应该使用哪个请求的响应作为最终的数据结果?。答案使用最后一个触发的请求的响应作为最终的结果,因为这是最新的,最具时效性的数据。
在上述 fetch user 的例子中,我们应该丢弃 fetch user2 的响应,因为我们最后操作的是 fetch user 3,页面最终应该展示的是 user3 的数据。我们同样使用 cleanup 函数来解决网络竞态问题,写法和 case1 类似,如下:
// ...
useEffect(() => {let isCancelled = falsefetch(`https://jsonplaceholder.typicode.com/users/${id}`).then(res => res.json()).then(data => {if (!isCancelled) {setCurUser(data)}})return () => {isCancelled = true}}, [id])
// ...
前面讲到, cleanup 函数的另一个执行时机是:每个依赖项变更后,页面重新渲染前运行一次,所以我们快速点击 fetch user2、fetch user3,id 从 2 到 3,那么id 为 2 时对应的 isCancelled 变量被设置为了 true,所以此时就不会进入 if (!isCancelled) {} 条件语句中执行 setCurUser(data),那么 id 为 2 的这条响应就被丢弃了,最终页面就不会先展示 user2 的信息再展示 user3 的信息。最终结果视频所示:
网络请求竞态视频2
拓展
前面我们讲了如何借助 cleanup 函数来丢弃响应,但可能很多时候,对于无效的请求,我们希望能取消它,对于 fetch 请求,我们借助 fetch 的第二个参数中 signal 和 AbortController 实现,如下:
// ...
useEffect(() => {let controller = new AbortController()let signal = controller.signalfetch(`https://jsonplaceholder.typicode.com/users/${id}`,{signal}).then(res => res.json()).then(data => {setCurUser(data)})return () => {// 在 cleanup 函数中终止当前请求controller.abort()}
}, [id])// ...
结果如下:
取消网络请求
在上述视频中,我们看到在点击 fetch user3 之后,fetch user2 的请求状态变成了已取消。
在平时的开发中,我们一般会用 axios 进行请求,下面是 axios 取消请求的例子:
import axios from 'axios'
// ...
useEffect(() => {const CancelToken = axios.CancelTokenconst source = CancelToken.source()axios.get(`https://jsonplaceholder.typicode.com/users/${id}`,{cancelToken: source.token}).then(res => {setCurUser(res.data)})return () => {source.cancel()}
}, [id])// ...
相关文章:
useEffect 不可忽视的 cleanup 函数
在 react 开发中, useEffect 是我们经常会使用到的钩子,一个基础的例子如下: useEffect(() > {// some code here// cleanup 函数return () > {doSomething()} }, [dependencies])上述代码中, cleanup 函数的执行时机有如下…...
vue3:使用:批量删除功能
场景:vue中使用el-table,常需要记住上一页所勾选的数据,批量删除操作,或者弹窗分页勾选,进行第一页勾选,在调后端接口选择第二页勾选其他数据。 1、element-ui 的table表格可以轻松实现多选的功能,只要在表…...
Scala中的样例类和样例对象和JAVA存根类
Scala中的样例类和样例对象 在 Scala 中,样例类(case class)和样例对象(case object)都是用于定义不可变数据类型的特殊类和对象。它们被广泛用于模式匹配、代数数据类型(Algebraic Data Types)…...
【0218】当SIGQUIT kill掉stats collector后,stats collector如何保存最终统计数据
1. stats collector可被哪些信号给kill? stats collector进程的主体函数是 PgstatCollectorMain(),该函数内部完成了stats collector进程的信号注册、现有统计文件读取、消息处理等任务。 忽略通常与postmaster中的某些操作绑定的所有信号,SIGHUP和SIGQUIT除外。 注意,我们…...
httplib 与 json.hpp 结合示例
httplib 与 json.hpp 结合示例 1、使用POST 接口,发送 登陆 请求 客户端发送 {nlohmann::json jsonOfCollectionInfo;jsonOfCollectionInfo["user_id"] "zhang";jsonOfCollectionInfo["password"] "123456";httplib::…...
RK3288安卓7.1开机上电到显示logo需要在3s内完成
需求: 从上电到开始开机logo有一段黑屏时间,这个黑屏时间大概在6s左右,给客户体验很不好,现在需要将这段黑屏时间缩短到2-3s左右 思路: 因为只需要早点显示logo,其实整体从上电到开机动画到安卓系统启动整体…...
Maven之hibernate-validator 高版本问题
hibernate-validator 高版本问题 hibernate-validator 的高版本(邮箱注解)依赖于高版本的 el-api,tomcat 8 的 el-api 是 3.0,满足需要。但是 tomcat 7 的 el-api 只有 2.2,不满足其要求。 解决办法有 2 种ÿ…...
C++--动态规划其他问题
1.一和零 力扣(LeetCode)官网 - 全球极客挚爱的技术成长平台 给你一个二进制字符串数组 strs 和两个整数 m 和 n 。 请你找出并返回 strs 的最大子集的长度,该子集中 最多 有 m 个 0 和 n 个 1 。 如果 x 的所有元素也是 y 的元素࿰…...
PostgreSQL 查询语句大全
🌷🍁 博主猫头虎(🐅🐾)带您 Go to New World✨🍁 🦄 博客首页——🐅🐾猫头虎的博客🎐 🐳 《面试题大全专栏》 🦕 文章图文…...
扫盲:常用NoSQL数据库
前言 关系型数据库产品很多,如 MySQL、Oracle、Microsoft SQL Sever 等,但它们的基本模型都是关系型数据模型。 非关系型数据库又称为:NoSQL ,没有统一的模型,而且是非关系型的。 常见的 NoSQL 数据库包括键值数据库、…...
MPI之数据打包和解包
MPI_Pack 和 MPI_Unpack 它们可以将源数据打包成二进制格式以便于传输,或者将二进制格式的数据解包成目标数据。这对函数通常用于在 MPI 应用程序中进行异构系统间的通信,即两个系统之间使用不同的二进制格式进行交互通信。 打包(序列化&…...
9.2作业
QT实现闹钟 widget.h #ifndef WIDGET_H #define WIDGET_H#include <QWidget> #include<QTimerEvent> #include<QDateTime> #include<QLineEdit> #include<QLabel> #include<QPushButton> #include <QTextToSpeech> QT_BEGIN_NAMES…...
数据库建设命名规范
1、数据库库表命名规范 1.1 数据库命名规范 采用26个英文字母(区分大小写)和0-9的自然数(经常不需要)加上下划线_组成,命名简洁明确,多个单词用下划线_分隔,一个项目一个数据库,多个项目慎用同一个数据库全部小写命名,禁止出现大…...
单元测试及其工具Junit
1.单元测试是什么 单元测试是开发者编写的一小段代码,用于检验被测代码的一个很小的、很明确的功能是否正确,通常而言,一个单元测试是用于判断某个特定条件(或者场景)下某个特定函数的行为。 单元测试是软件测试的一种…...
Multicast IP Interface
该模块通过多播IPv4和IPv6在UDP上实现CAN和CAN FD消息的传输。此虚拟接口允许在多个进程甚至主机之间进行通信。这与虚拟接口不同,虚拟接口只能在单个进程中传递消息,但不需要网络堆栈。 它在UDP上运行以具有尽可能低的延迟(与使用TCP相反),并且因为正常的IP多播本质上是…...
从零学算法2833
2833.给你一个长度为 n 的字符串 moves ,该字符串仅由字符 ‘L’、‘R’ 和 ‘’ 组成。字符串表示你在一条原点为 0 的数轴上的若干次移动。 你的初始位置就在原点(0),第 i 次移动过程中,你可以根据对应字符选择移动方…...
python安装cfg模块时报错,ERROR: No matching distribution found for cfg
使用pip3 install cfg2 才行 pip3 install cfg2...
优思学院|六西格玛中的概率分布有哪些?
为什么概率分布重要? 概率分布是统计学中一个重要的概念,它帮助我们理解随机变量的分布情况以及与之相关的概率。在面对具体问题时,了解概率分布可以帮助我们选择适当的检验或分析策略,以解决问题并做出合理的决策。 常见的概率…...
工控上位机程序为什么只能用C语言?
工控上位机程序并不只能用C#开发,实际上在工业自动化领域中,常见的上位机开发语言包括但不限于以下几种:C#: C#是一种常用的编程语言,在工控领域中被广泛使用。它具有良好的面向对象特性和丰富的类库支持,可以实现高性…...
Go操作各大消息队列教程(RabbitMQ、Kafka)
Go操作各大消息队列教程 1 RabbitMQ 1.1 概念 ①基本名词 当前市面上mq的产品很多,比如RabbitMQ、Kafka、ActiveMQ、ZeroMQ和阿里巴巴捐献给Apache的RocketMQ。甚至连redis这种NoSQL都支持MQ的功能。 Broker:表示消息队列服务实体Virtual Host&#x…...
多模态2025:技术路线“神仙打架”,视频生成冲上云霄
文|魏琳华 编|王一粟 一场大会,聚集了中国多模态大模型的“半壁江山”。 智源大会2025为期两天的论坛中,汇集了学界、创业公司和大厂等三方的热门选手,关于多模态的集中讨论达到了前所未有的热度。其中,…...
【HarmonyOS 5.0】DevEco Testing:鸿蒙应用质量保障的终极武器
——全方位测试解决方案与代码实战 一、工具定位与核心能力 DevEco Testing是HarmonyOS官方推出的一体化测试平台,覆盖应用全生命周期测试需求,主要提供五大核心能力: 测试类型检测目标关键指标功能体验基…...
解锁数据库简洁之道:FastAPI与SQLModel实战指南
在构建现代Web应用程序时,与数据库的交互无疑是核心环节。虽然传统的数据库操作方式(如直接编写SQL语句与psycopg2交互)赋予了我们精细的控制权,但在面对日益复杂的业务逻辑和快速迭代的需求时,这种方式的开发效率和可…...
2.Vue编写一个app
1.src中重要的组成 1.1main.ts // 引入createApp用于创建应用 import { createApp } from "vue"; // 引用App根组件 import App from ./App.vue;createApp(App).mount(#app)1.2 App.vue 其中要写三种标签 <template> <!--html--> </template>…...
Matlab | matlab常用命令总结
常用命令 一、 基础操作与环境二、 矩阵与数组操作(核心)三、 绘图与可视化四、 编程与控制流五、 符号计算 (Symbolic Math Toolbox)六、 文件与数据 I/O七、 常用函数类别重要提示这是一份 MATLAB 常用命令和功能的总结,涵盖了基础操作、矩阵运算、绘图、编程和文件处理等…...
MySQL 8.0 OCP 英文题库解析(十三)
Oracle 为庆祝 MySQL 30 周年,截止到 2025.07.31 之前。所有人均可以免费考取原价245美元的MySQL OCP 认证。 从今天开始,将英文题库免费公布出来,并进行解析,帮助大家在一个月之内轻松通过OCP认证。 本期公布试题111~120 试题1…...
select、poll、epoll 与 Reactor 模式
在高并发网络编程领域,高效处理大量连接和 I/O 事件是系统性能的关键。select、poll、epoll 作为 I/O 多路复用技术的代表,以及基于它们实现的 Reactor 模式,为开发者提供了强大的工具。本文将深入探讨这些技术的底层原理、优缺点。 一、I…...
短视频矩阵系统文案创作功能开发实践,定制化开发
在短视频行业迅猛发展的当下,企业和个人创作者为了扩大影响力、提升传播效果,纷纷采用短视频矩阵运营策略,同时管理多个平台、多个账号的内容发布。然而,频繁的文案创作需求让运营者疲于应对,如何高效产出高质量文案成…...
Mysql中select查询语句的执行过程
目录 1、介绍 1.1、组件介绍 1.2、Sql执行顺序 2、执行流程 2.1. 连接与认证 2.2. 查询缓存 2.3. 语法解析(Parser) 2.4、执行sql 1. 预处理(Preprocessor) 2. 查询优化器(Optimizer) 3. 执行器…...
Go 语言并发编程基础:无缓冲与有缓冲通道
在上一章节中,我们了解了 Channel 的基本用法。本章将重点分析 Go 中通道的两种类型 —— 无缓冲通道与有缓冲通道,它们在并发编程中各具特点和应用场景。 一、通道的基本分类 类型定义形式特点无缓冲通道make(chan T)发送和接收都必须准备好࿰…...
