如果你用React写过副作用代码,一定见过"清理函数"这个概念。它看起来简单,但很多人直到踩了内存泄漏的坑才真正理解它的价值。这篇文章用1个真实的请求竞态案例,帮你彻底搞懂什么时候必须用清理函数。
清理函数到底是什么
![]()
清理函数是你从useEffect里return的那个函数。React会在2种情况下调用它:组件卸载时,或者副作用重新执行前。它的作用很直接——停止定时器、取消未完成的请求、移除事件监听。
React的StrictMode在开发环境下会故意把组件挂载、卸载、再挂载1次,就是为了逼你写出正确的清理逻辑。不是每个副作用都需要清理,只有那些"启动了一个持续过程"的才需要,比如订阅、轮询、或者未完成的网络请求。没写清理函数的后果很实在:UI状态错乱,或者内存泄漏。
1个会出错的代码示例
假设你要做一个分页评论列表,带1个下拉菜单选择从哪条开始加载。代码结构大概是这样:
先用setTimeout模拟3秒延迟,模拟慢网络环境。然后写3个state:dataDisplay存评论数据,loading控制加载状态,start记录当前起始位置。
useEffect监听start变化,每次变化就setLoading(true),调用fetchComments,拿到数据后setDisplay,最后setLoading(false)。看起来没问题,但这里埋着1个经典bug。
竞态问题是怎么发生的
假设用户先选了"从第0条开始",请求发出,3秒后才能返回。用户在第1秒又选了"从第20条开始",新请求发出。这时候有2个请求在跑:A请求(0条开始)和B请求(20条开始)。
如果B请求先回来,UI显示第20条开始的数据。然后A请求回来了,它会覆盖掉B请求的结果,UI跳回第0条开始的数据——但用户的下拉菜单还显示着"20条开始"。这就是请求竞态,状态和数据对不上了。
用清理函数解决竞态
解决方案是给每个请求1个"身份证",让过期的请求自动失效。具体做法:
1. 在useEffect里创建1个AbortController实例
2. 把它的signal传给fetch的第2个参数
3. return1个函数,调用controller.abort()
代码变成这样:
useEffect(() => {const controller = new AbortController();setLoading(true);fetch(`https://dummyjson.com/comments?skip=${start}`, {signal: controller.signal.then(res => res.json()).then(data => setDisplay(data.comments)).catch(err => {if (err.name === 'AbortError') return; // 忽略取消错误console.error(err);.finally(() => setLoading(false));return () => controller.abort();}, [start]);现在当start变化,React会先执行上1次的清理函数,把上1个请求取消掉。那个3秒前的请求即使回来了,也会被浏览器丢弃,不会触发then回调。UI永远只显示最后1次选择对应的数据。
还有3种场景必须用清理函数
定时器和轮询:setInterval必须配clearInterval,否则组件卸载后定时器还在跑,造成内存泄漏。
事件监听:addEventListener后要removeEventListener,特别是window或document上的全局事件。
WebSocket连接:连接建立后必须关闭,否则连接数会持续累积。
清理函数的本质是"撤销副作用"。只要你的副作用在组件外留下了1个持续存在的状态,就需要在清理函数里把它恢复原状。这是React组件正确卸载的最后1道防线。
特别声明:以上内容(如有图片或视频亦包括在内)为自媒体平台“网易号”用户上传并发布,本平台仅提供信息存储服务。
Notice: The content above (including the pictures and videos if any) is uploaded and posted by a user of NetEase Hao, which is a social media platform and only provides information storage services.