React state update memory leak 内存溢出问题与解决方案

问题背景

在测试 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 观察内存的使用情况,发现在数据加载完之前就切换页面,内存使用量升上去之后就不会降下来了,表示有内存泄漏的现象。

JS 内存使用情况

2 种常见情况

有 2 种常见的使用情况非常容易引起该问题。

  1. 使用了 setTimeout(),并且在回调函数里执行 setState()。如果在回调函数被执行之前就离开页面导致目标组件被卸载了的话,那么当回调函数被执行之后就会发生内存溢出的错误。
setTimeout(function () {
	this.setState(data);
}, 1000);
  1. 在异步请求的回调函数中执行了 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() 里就可以了。

扫描二维码关注公众号,回复: 8788111 查看本文章
// 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();
	};
}, []);
发布了14 篇原创文章 · 获赞 8 · 访问量 2203

猜你喜欢

转载自blog.csdn.net/vandavidchou/article/details/103382877
今日推荐