问题背景
在测试 React 页面时,发现在一个页面的数据加载完之前,马上切换到另一个页面,React 会在控制台给出如下警告:
Warning: Can’t perform a React state update on an unmounted component.
This is a no-op, but it indicates a memory leak in your application.
To fix, cancel all subscriptions and asynchronous tasks in a useEffect
cleanup function.
问题原因
引发这个问题的情况很多,但归根结底就一个原因:
React 在已经卸载 (unmounted) 的组件上执行 setState() 操作。
通过 Chrome 浏览器的 Developer Tool 观察内存的使用情况,发现在数据加载完之前就切换页面,内存使用量升上去之后就不会降下来了,表示有内存泄漏的现象。
2 种常见情况
有 2 种常见的使用情况非常容易引起该问题。
- 使用了 setTimeout(),并且在回调函数里执行 setState()。如果在回调函数被执行之前就离开页面导致目标组件被卸载了的话,那么当回调函数被执行之后就会发生内存溢出的错误。
setTimeout(function () {
this.setState(data);
}, 1000);
- 在异步请求的回调函数中执行了 setState(),在回调函数执行完成之前目标组件就被卸载了。这里我们以 axios 的异步请求为例:
axios.get(url)
.then(response => {
this.setState(response.data);
});
解决办法
解决的办法就是确保在组件被卸载的时候,跟该组件有关的 setTimeout() 以及异步请求也要一起被清除。
我们都知道 React 有两种类型的组件:
- Class Component
- Functional Component
下面会分别演示两种类型组件的解决方案。
解决 setTimeout() 内存溢出问题
在这里我们可以使用 clearTimeout() 函数来清除我们之前执行的 setTimeout()。由于 setTimeout() 会传回一个 timeout id,我们只需要将这个 timeout id 传入 clearTimeout() 里就可以了。
// Class Component
componentDidMount() {
this.timeout = setTimeout(function () {
setState(data);
}, 1000);
}
componentWillUnmount() {
// 组件即将卸载时先清除 setTimeout()
clearTimeout(this.timeout);
}
// Functional Component
useEffect(() => {
const timeout = setTimeout(function () {
setState(data);
}, 1000);
// 返回 clean up 函数。在组件即将被卸载时,React 会执行该函数
return () => {
clearTimeout(timeout);
};
}, []);
解决异步请求造成的内存溢出
这里我们有两个解决方案,对于 Class Component,我们可以设定一个值来确定组件是否已加载。如果该值为 false 的话则我们的异步请求回调函数直接返回,不做任何操作。
// Class Component
constructor(props) {
super(props);
// 该值表示组件是否已加载
this._isMounted = false;
}
componentDidMount() {
// 组件已加载,设为 true
this._isMounted = true;
axios.get(url)
.then(data => {
// 如果组件还没加载,或已经卸载,则直接返回
if (!this._isMounted) {
return;
}
// ... rest of codes
});
}
componentWillUnmount() {
// 组件即将卸载,将值设为 false
this._isMounted = false;
}
对与 Functional Component 来说,我们无法使用以上的方法。因为我们没有一个对象可以帮我们保存 this._isMounted 的值,每一次组件重新渲染的时候,Functional Component 的代码都会被重新运行一遍,导致我们设定的 isMounted 值被刷新。
如果我们的异步请求是使用 axios 的话,我们可以使用 axios 提供的 cancel token 来取消异步请求。
// Functional Component
const source = axios.CancelToken.source();
const cancelToken = .source.token;
useEffect(() => {
axios.get(url, {
cancelToken: cancelToken
})
.then(data => {
// ... working with states
})
.catch(error => {
// 如果取消异步请求的话,一定要写出 catch 块,才不会出现 uncaught exception 的错误
});
return () => {
// 组件即将卸载时,取消异步请求
source.cancel();
};
}, []);