清理函数与请求竞态
effect 可以返回一个清理函数:组件卸载时、或下一次 effect 执行前会被调用。定时器要 clearTimeout、订阅要 removeEventListener、 WebSocket 要 close——有“开启”就必须有“清理”,否则会内存泄漏与重复执行。
典型场景:组件卸载后请求才返回 → setState 作用在已卸载组件上(React 18 已不再警告,但仍浪费)。正确做法是用清理函数配合 AbortController 取消未完成的请求,顺便解决“快速切换时旧请求覆盖新结果”的竞态问题。
- return 的清理函数在下一次 effect 执行前、组件卸载时调用
- 有开启就有清理:定时器、订阅、监听、请求
- AbortController 取消请求:防卸载后更新 + 防竞态
HTML
<!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>清理函数</title> <script src="https://unpkg.com/react@18/umd/react.development.js" crossorigin></script> <script src="https://unpkg.com/react-dom@18/umd/react-dom.development.js" crossorigin></script> <script src="https://unpkg.com/@babel/standalone/babel.min.js"></script> </head> <body> <div id="root"></div> <script type="text/babel"> const { useState, useEffect } = React; function Timer() { const [sec, setSec] = useState(0); useEffect(() => { const id = setInterval(() => setSec(s => s + 1), 1000); // 清理函数:组件卸载时停掉定时器,否则一直空跑 return () => clearInterval(id); }, []); return <p>已显示 {sec} 秒</p>; } function App() { const [show, setShow] = useState(true); const [q, setQ] = useState('React'); // 用 AbortController 取消旧请求:防竞态 + 防卸载后更新 useEffect(() => { const ctrl = new AbortController(); fetch('https://httpbin.org/get?q=' + q, { signal: ctrl.signal }) .then(r => r.json()) .then(() => console.log('查询完成:', q)) .catch(e => { if (e.name !== 'AbortError') console.error(e); }); return () => ctrl.abort(); // q 变化或卸载时取消上一个 }, [q]); return ( <div style={{ padding: 16 }}> <button onClick={() => setShow(s => !s)}>{show ? '卸载' : '挂载'}计时器</button> {show && <Timer />} <hr /> <input value={q} onChange={e => setQ(e.target.value)} /> <p>快速输入时,只会保留最后一次请求的结果</p> </div> ); } ReactDOM.createRoot(document.getElementById('root')).render(<App />); </script> </body> </html>
本节要点有“开”必有“清”:return 清理函数在卸载/重跑前调用;请求用 AbortController 取消防竞态。




