一、定时器未清除(最常见 🚨)
❌ 问题场景
useEffect(() => {
const timer = setInterval(() => {
console.log('running');
}, 1000);
}, []);
👉 组件卸载后,定时器还在执行
✅ 正确写法
useEffect(() => {
const timer = setInterval(() => {
console.log('running');
}, 1000);
return () => {
clearInterval(timer);
};
}, []);
⚠️ 延伸(容易忽略)
setTimeoutInteractionManager.runAfterInteractions
const task = InteractionManager.runAfterInteractions(() => {});
return () => task.cancel();
二、事件监听未移除
❌ 问题场景
useEffect(() => {
Dimensions.addEventListener('change', handler);
}, []);
👉 handler 持续存在,组件销毁后仍触发
✅ 正确写法
useEffect(() => {
const subscription = Dimensions.addEventListener('change', handler);
return () => {
subscription?.remove();
};
}, []);
⚠️ 常见泄漏来源
DimensionsKeyboardAppStateBackHandlerDeviceEventEmitter
三、异步请求未取消(非常隐蔽 ⚠️)
❌ 问题场景
useEffect(() => {
fetchData().then(res => {
setData(res);
});
}, []);
👉 组件已经卸载,但请求返回后仍然 setState
✅ 方案一:标记是否卸载(推荐简单方案)
useEffect(() => {
let isMounted = true;
fetchData().then(res => {
if (isMounted) {
setData(res);
}
});
return () => {
isMounted = false;
};
}, []);
✅ 方案二:AbortController(更优雅)
useEffect(() => {
const controller = new AbortController();
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data))
.catch(err => {
if (err.name !== 'AbortError') {
console.error(err);
}
});
return () => controller.abort();
}, []);
⚠️ axios 方案
const source = axios.CancelToken.source();
axios.get(url, {
cancelToken: source.token
});
return () => {
source.cancel();
};
四、闭包导致的内存泄漏(高级但常见)
❌ 问题
函数持有旧 state 引用,导致内存无法释放
useEffect(() => {
const interval = setInterval(() => {
console.log(count); // 旧值
}, 1000);
}, []);
✅ 解决方案
👉 使用 useRef
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
useEffect(() => {
const interval = setInterval(() => {
console.log(countRef.current);
}, 1000);
return () => clearInterval(interval);
}, []);
五、全局变量 / 单例引用
❌ 问题
global.cache = largeObject;
👉 永远不会释放
✅ 解决
- 避免存储大对象
- 使用弱引用(如果有)
- 页面卸载时手动清理
六、动画未停止(RN特有)
❌ 问题
Animated.timing(value, {
toValue: 1,
duration: 1000,
useNativeDriver: true
}).start();
👉 页面卸载仍执行动画
✅ 解决
const animation = Animated.timing(...);
animation.start();
return () => {
animation.stop();
};
七、FlatList / 大列表引起的“伪内存泄漏”
不是严格泄漏,但表现类似:
❌ 问题
- 渲染过多 item
- key 不稳定
- renderItem 频繁创建
✅ 优化
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={item => item.id}
removeClippedSubviews
windowSize={5}
initialNumToRender={10}
maxToRenderPerBatch={10}
/>
八、第三方库未正确释放
比如:
- WebSocket
- 地图 SDK
- 视频播放器
✅ 标准做法
useEffect(() => {
const socket = new WebSocket(url);
return () => {
socket.close();
};
}, []);
🔥 总结一套“防泄漏原则”
你可以记住这 5 条核心原则:
✅ 1. 有副作用,就必须有清理
- 定时器
- 监听器
- 动画
- socket
👉 一律 return cleanup
✅ 2. 异步请求必须可控
- 标记 isMounted
- 或 AbortController
✅ 3. 不在卸载后 setState
✅ 4. 避免闭包持有旧引用
✅ 5. 大对象谨慎缓存
🚀 给你一个工程级最佳实践模板
useEffect(() => {
let isMounted = true;
const controller = new AbortController();
const timer = setInterval(() => {}, 1000);
const subscription = Dimensions.addEventListener('change', () => {});
fetch(url, { signal: controller.signal })
.then(res => res.json())
.then(data => {
if (isMounted) setData(data);
});
return () => {
isMounted = false;
controller.abort();
clearInterval(timer);
subscription?.remove();
};
}, []);