当前位置: 首页 > news >正文

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])// ...
http://www.lryc.cn/news/150795.html

相关文章:

  • vue3:使用:批量删除功能
  • Scala中的样例类和样例对象和JAVA存根类
  • 【0218】当SIGQUIT kill掉stats collector后,stats collector如何保存最终统计数据
  • httplib 与 json.hpp 结合示例
  • RK3288安卓7.1开机上电到显示logo需要在3s内完成
  • Maven之hibernate-validator 高版本问题
  • C++--动态规划其他问题
  • PostgreSQL 查询语句大全
  • 扫盲:常用NoSQL数据库
  • MPI之数据打包和解包
  • 9.2作业
  • 数据库建设命名规范
  • 单元测试及其工具Junit
  • Multicast IP Interface
  • 从零学算法2833
  • python安装cfg模块时报错,ERROR: No matching distribution found for cfg
  • 优思学院|六西格玛中的概率分布有哪些?
  • 工控上位机程序为什么只能用C语言?
  • Go操作各大消息队列教程(RabbitMQ、Kafka)
  • 对话出海企业:2023亚马逊云科技出海日圆桌论坛
  • 【图解算法数据结构】分治算法篇 + Java代码实现
  • Ubuntu系统环境搭建(八)——Ubuntu开机自动执行命令
  • c++(8.29)auto关键字,lambda表达式,数据类型转换,标准模板库,list,文件操作+Xmind
  • Docker学习笔记(持续更新)
  • 无涯教程-Android - 应用组件
  • 树与图c++
  • 中小企业常用的 IT 项目管理软件有哪些?
  • 汇编原理计算方法:物理地址=段地址*16+偏移地址
  • jdk-8u371-linux-x64.tar.gz jdk-8u371-windows-x64.exe 【jdk-8u371】 全平台下载
  • 数据结构体--5.0图