useEffect 不可忽视的 cleanup 函数

文章讲述了在React开发中,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 = false
   fetch('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 = false
     fetch(`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.signal
   
   fetch(`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.CancelToken
   const source = CancelToken.source()
   
  axios.get(`https://jsonplaceholder.typicode.com/users/${id}`,{
    cancelToken: source.token
  })
   .then(res => {
       setCurUser(res.data)
   })
  
   return () => {
       source.cancel()
   }
}, [id])

// ...
<think>好的,我需要处理用户关于cleanup函数的使用或定义的查询。首先,用户提到了两个引用:一个是关于ngx_pool_cleanup_add的使用和原理,另一个是关于pthread_cleanup_push和pthread_cleanup_pop函数的线程清理机制。用户还希望我参考他们之前的问题和回答,尽可能多地结合这些信息。 首先,我需要明确用户的需求。他们想了解cleanup函数的使用方法和定义,可能是在不同的上下文中,比如内存池管理或线程清理。因此,我需要分别解释这两个场景下的cleanup函数。 首先,处理Nginx内存池中的cleanup函数。根据引用[1],ngx_pool_cleanup_add用于分配清理结构体和相关参数的空间,并在销毁内存池时执行清理操作。我需要详细说明其使用步骤,包括分配、设置处理函数和数据,以及自动或手动触发清理。同时,需要强调内存池销毁时自动调用的机制。 接下来是线程清理函数,引用[2]提到了pthread_cleanup_push和pthread_cleanup_pop。用户特别问到为什么pop必须写上,这可能是因为宏定义需要匹配大括号。我需要解释这两个函数的作用,使用场景,以及为何必须成对使用,并说明参数execute的作用。还要举例说明在线程取消或退出时的行为。 用户之前的问题涉及C/C++的时间计算和消息框功能,但当前问题转向cleanup函数,所以需要确保回答专注于清理机制,但可以适当关联到资源管理的通用概念,比如释放资源、避免泄漏等。 需要检查是否有其他可能的cleanup函数上下文,比如其他库或框架中的使用,但根据现有引用,应集中在Nginx和pthreads。确保回答结构清晰,分点说明,每个部分有示例代码,帮助用户理解应用场景。 可能的问题点:用户可能混淆不同环境中的cleanup函数,需要明确区分内存池清理和线程清理。同时,要验证示例代码的正确性,避免语法错误,特别是宏的使用,如pthread_cleanup_push和pop需要成对出现,并且注意作用域。 最后,生成相关问题,帮助用户深入理解,比如如何传递参数、处理异常情况,或者在不同平台下的差异。确保引用正确,如引用[1]和[2]的信息,并在回答中标明出处。</think>--- ### Cleanup函数详解:内存管理与线程清理 Cleanup函数在不同场景下有不同的实现方式,主要分为**资源释放回调**和**线程清理**两类。以下结合Nginx内存池和POSIX线程库的典型实现进行说明。 --- ### 一、Nginx内存池清理函数 #### 1. **核心函数定义** - **`ngx_pool_cleanup_add`** 用于向内存池注册清理回调函数,在内存池销毁时自动触发资源释放。 ```c ngx_pool_cleanup_t *ngx_pool_cleanup_add(ngx_pool_t *pool, size_t size); ``` - **参数**: - `pool`: 关联的内存池对象 - `size`: 自定义数据结构的额外分配空间大小(通过`data`字段访问) - **返回值**:`ngx_pool_cleanup_t`结构指针 - **清理结构体定义**: ```c typedef struct ngx_pool_cleanup_s ngx_pool_cleanup_t; struct ngx_pool_cleanup_s { ngx_pool_cleanup_pt handler; // 清理回调函数指针 void *data; // 传递给handler的参数 ngx_pool_cleanup_t *next; // 链表结构支持多清理函数 }; ``` #### 2. **使用步骤** 1. **注册清理函数** 分配清理结构体并设置回调逻辑: ```c ngx_pool_cleanup_t *cln = ngx_pool_cleanup_add(pool, sizeof(my_data_t)); cln->handler = my_cleanup_handler; // 自定义清理函数 my_data_t *mydata = cln->data; // 访问额外分配的空间 ``` 2. **定义清理函数** 实现资源释放逻辑(如关闭文件、释放外部资源): ```c void my_cleanup_handler(void *data) { my_data_t *mydata = data; if (mydata->fd != -1) { close(mydata->fd); // 关闭文件描述符 } } ``` 3. **触发清理** 调用`ngx_destroy_pool(pool)`时自动执行所有注册的清理函数[^1]。 --- ### 二、POSIX线程清理函数 #### 1. **函数原型** ```c void pthread_cleanup_push(void (*routine)(void*), void *arg); void pthread_cleanup_pop(int execute); ``` #### 2. **核心特性** - **作用域绑定**:必须成对使用`push`/`pop`,且在同一代码块内(因宏展开依赖`{`和`}`)[^2] - **触发条件**: - 线程调用`pthread_exit()` - 其他线程调用`pthread_cancel()`终止当前线程 - 显式调用`pthread_cleanup_pop(1)` #### 3. **使用示例** ```c void cleanup_func(void *arg) { printf("释放资源: %s\n", (char*)arg); } void* thread_func(void *arg) { FILE *fp = fopen("data.txt", "r"); pthread_cleanup_push(cleanup_func, "文件资源"); // 注册清理 if (fp == NULL) { pthread_exit(NULL); // 触发清理函数 } // 临界区操作... pthread_cleanup_pop(1); // 执行清理(参数1表示执行) return NULL; } ``` --- ### 三、对比分析 | 特性 | Nginx内存池清理 | POSIX线程清理 | |-----------------------|-----------------------------------|----------------------------------| | **触发时机** | 内存池销毁时自动触发 | 线程终止或显式调用`pop(1)`时触发 | | **数据结构** | 链表管理多清理函数 | 栈结构(后进先出) | | **参数传递** | 通过`data`字段传递自定义数据 | 使用`pthread_cleanup_push`的arg参数 | | **典型应用场景** | 释放内存池关联的外部资源 | 线程异常退出时的资源回滚 | --- ### 四、高级应用技巧 1. **嵌套清理函数** POSIX线程允许嵌套调用`push`/`pop`,资源释放顺序遵循栈规则: ```c pthread_cleanup_push(func1, "Level1"); pthread_cleanup_push(func2, "Level2"); // ...操作代码 pthread_cleanup_pop(1); // 先执行func2 pthread_cleanup_pop(1); // 再执行func1 ``` 2. **动态参数传递** Nginx内存池通过`data`字段支持动态数据结构: ```c typedef struct { int fd; char *filename; } file_ctx_t; file_ctx_t *ctx = cln->data; ctx->fd = open(ctx->filename, O_RDONLY); ``` 3. **调试支持** 在清理函数中添加日志输出,跟踪资源生命周期: ```c void debug_cleanup(void *arg) { printf("[DEBUG] 释放资源: %p\n", arg); free(arg); } ``` --- ### 五、相关问题 1. 如何防止cleanup函数被重复调用? 2. 在Windows系统中如何实现类似pthread的线程清理机制? 3. Nginx内存池清理函数能否用于非内存资源(如网络连接)?
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值