React 19 入门教程
依赖与清理
本教程共 50 篇 · 第 28 篇 · 更新于 2026-07-29 · 约 7 分钟阅读
ReactuseEffect清理函数依赖数组竞态
28. 依赖与清理
本节目标:掌握依赖数组的完整规则,学会写清理函数,避免无限循环和竞态问题。
依赖数组规则
effect 里用到的所有响应式值都要放进依赖数组。
响应式值包括:
- props
- state
- 组件内声明的变量和函数
function SearchResults({ query }) {
const [results, setResults] = useState([]);
const [page, setPage] = useState(1);
const apiKey = 'xxx'; // 常量,不是响应式值
useEffect(() => {
fetch(`/api/search?q=${query}&page=${page}&key=${apiKey}`)
.then(res => res.json())
.then(data => setResults(data));
}, [query, page]); // apiKey 不是响应式值,不需要放
}
ESLint 会帮你检查
装
eslint-plugin-react-hooks后,react-hooks/exhaustive-deps规则会自动提示遗漏的依赖。别忽略它。
空依赖数组
effect 不依赖任何响应式值时,用空数组:
useEffect(() => {
const canvas = document.getElementById('canvas');
// 初始化画布,只执行一次
}, []);
空数组意味着”只在挂载时执行,卸载时清理”。
没有依赖数组
不设依赖数组,每次渲染后都执行:
useEffect(() => {
console.log('每次渲染都跑');
});
大多数情况这是bug。如果你确实需要,想清楚为什么。
清理函数
有些 effect 需要”撤销”之前的操作。返回一个清理函数:
useEffect(() => {
// 开始
const timer = setInterval(() => tick(), 1000);
// 清理(返回这个函数)
return () => {
clearInterval(timer);
};
}, []);
清理函数的执行时机:
- 重新同步前:依赖变化时,先清理旧的,再执行新的
- 卸载时:组件移除时,执行清理
定时器的清理
function Timer() {
const [count, setCount] = useState(0);
useEffect(() => {
const id = setInterval(() => {
setCount(c => c + 1);
}, 1000);
return () => clearInterval(id);
}, []);
return <p>{count}</p>;
}
不设清理函数,组件卸载后定时器还在跑,会尝试更新不存在的状态,导致内存泄漏。
事件监听的清理
function WindowSize() {
const [size, setSize] = useState({
width: window.innerWidth,
height: window.innerHeight
});
useEffect(() => {
function handleResize() {
setSize({
width: window.innerWidth,
height: window.innerHeight
});
}
window.addEventListener('resize', handleResize);
return () => {
window.removeEventListener('resize', handleResize);
};
}, []);
return <p>{size.width} x {size.height}</p>;
}
不清理事件监听,组件卸载后回调还在,会内存泄漏。
竞态问题
快速切换时,后发的请求可能先返回,先发的反而后返回。结果被覆盖:
useEffect(() => {
fetch(`/api/users/${userId}`)
.then(res => res.json())
.then(data => setUser(data));
}, [userId]);
用户从用户A切换到B,A的请求可能比B晚回来,页面最后显示A的信息。
解决方案:用 AbortController 取消旧请求:
useEffect(() => {
const controller = new AbortController();
fetch(`/api/users/${userId}`, { signal: controller.signal })
.then(res => res.json())
.then(data => setUser(data))
.catch(err => {
if (err.name !== 'AbortError') throw err;
});
return () => controller.abort();
}, [userId]);
AbortController 是浏览器原生 API
不仅能取消 fetch,还能取消其他异步操作。是现代浏览器都支持的。
对象和函数作为依赖
每次渲染都创建新的对象/函数引用,导致 effect 无限执行:
// 问题:每次渲染 options 都是新对象
useEffect(() => {
fetch('/api', { body: JSON.stringify(options) });
}, [options]); // 无限循环!
// 解决:提取基本类型依赖
const { limit, offset } = options;
useEffect(() => {
fetch('/api', { body: JSON.stringify(options) });
}, [limit, offset]);
// 或者用 useMemo 缓存对象
const options = useMemo(() => ({ limit, offset }), [limit, offset]);
函数同理:
// 问题:每次渲染 handleFetch 都是新函数
useEffect(() => {
handleFetch();
}, [handleFetch]);
// 解决:用 useCallback 缓存函数
const handleFetch = useCallback(() => {
// ...
}, [deps]);
依赖速查
// 不依赖任何值
useEffect(() => { ... }, []);
// 依赖 props
useEffect(() => { ... }, [userId, roomId]);
// 依赖 state
useEffect(() => { ... }, [query, page]);
// 依赖 props + state
useEffect(() => { ... }, [props.id, state.filter]);
本节要点
- effect 里所有响应式值都要放进依赖数组
- 清理函数在重新同步前和卸载时执行
- 定时器、事件监听、网络请求都要清理
- 竞态问题用 AbortController 解决
- 对象/函数依赖用 useMemo/useCallback 缓存