背景
熟悉 React 的小伙伴对这个错误信息一定不陌生:
Warning: Can't call setState (or forceUpdate) 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 the componentWillUnmount method.
之前也看到过, 但是一直没处理。 早上趁喝茶的功夫决定看一下。
复现的路径: 在某个请求结果没回来之前就切换页面, 必现。
原因
这种问题出现一般是, 请求发出得到结果之后, 进行了 setState 的操作。 形如:
class Demo extends Component { constructor(props) { super(props); this.state = { news: [], }; } componentDidMount() { axios .get('https://hn.algolia.com/api/v1/search?query=react') .then(result => this.setState({ news: result.data, }), ); } render() { return ( <ul> {this.state.news.map(topic => ( <li key={topic.objectID}>{topic.title}</li> ))} </ul> ); }}
在结果回来之前, 我们切换路由, 组件销毁, 但是请求是异步的, 结果回来之后, 组件已经销毁了, 这个之后执行 setState 可能会有意想不到的后果, 正如error 信息提示的那样.
知道原因之后, 就比较好解决了:
- 在组件销毁之前, cancel 掉发出的请求。
找资料的过程中也发现了一些 脑洞大开的解决办法:
几种开脑洞的解决办法
标志位法(不推荐
)
这个方法的思路也很简单, 给个标志位, 比如叫 _isMounted
:
class Demo extends Component { _isMounted = false; constructor(props) { super(props); this.state = { news: [], }; } componentDidMount() { this._isMounted = true; axios .get('https://hn.algolia.com/api/v1/search?query=react') .then(result => { if (this._isMounted) { this.setState({ news: result.data.hits, }); } }); } componentWillUnmount() { this._isMounted = false; } render() { // ... }}
Unmount 之后, 标志位为false , 不执行setState. 自然也就不会报错。
顺着这个思路,干脆写个组件统一处理, 形如
import React from 'react';function inject_prevent_setState_after_unount(target) { let componentWillUnmount = target.prototype.componentWillUnmount; target.prototype.componentWillUnmount = function() { if (componentWillUnmount) componentWillUnmount.call(this, ...arguments); this.unmount = true; }; let setState = target.prototype.setState; target.prototype.setState = function() { if (this.unmount) return; setState.call(this, ...arguments); };}@inject_prevent_setState_after_unountclass BaseComponent extends React.Component {}export default BaseComponent;
然后在业务代码里直接继承这个组件, 这个和上面的标志位法其实是一个道理,虽然也能 hack 掉错误信息, 但是请求的副作用依旧会发生
,这种做法也是不推荐
的。
官网上也有对这个情景的描述:
详情请戳我
The primary use case for isMounted() is to avoid calling setState() after a component has unmounted, because calling setState() after a component has unmounted will emit a warning. The “setState warning” exists to help you catch bugs, because calling setState() on an unmounted component is an indication that your app/component has somehow failed to clean up properly. Specifically, calling setState() in an unmounted component means that your app is still holding a reference to the component after the component has been unmounted - which often indicates a memory leak!
To avoid the error message, people often add lines like this:
if (this.isMounted()) { // This is bad.
this.setState({...});
}
官网推荐的做法是: 在componentWillUnmount 的时候取消掉所有的请求。
形如:
class MyComponent extends React.Component { componentDidMount() { mydatastore.subscribe(this); } render() { ... } componentWillUnmount() { mydatastore.unsubscribe(this); }}
实现一个可以 cancel 的 Promise
我在这也给出一个简单的实现:
// cancelablePromise.jsexport const cancelablePromise = promise => { let hasCanceled = false; const wrappedPromise = new Promise((resolve, reject) => { promise.then( value => (hasCanceled ? reject({ isCanceled: true, value }) : resolve(value)), error => reject({ isCanceled: hasCanceled, error }), ); }); return { promise: wrappedPromise, cancel: () => (hasCanceled = true), };};
在你的组件中:
import React, { Component } from "react";import cancelablePromise from "./cancelable-promise";class MyComponent extends Component { state = { data: [], error: null, }; pendingPromises = []; componentWillUnmount = () => this.pendingPromises.map(p => p.cancel()); appendPendingPromise = promise => this.pendingPromises = [...this.pendingPromises, promise]; removePendingPromise = promise => this.pendingPromises = this.pendingPromises.filter(p => p !== promise); handleOnClick = () => { const wrappedPromise = cancelablePromise(fetchData()); this.appendPendingPromise(wrappedPromise); return wrappedPromise.promise .then(() => this.setState({ data })) .then(() => this.removePendingPromise(wrappedPromise)) .catch(errorInfo => { if (!errorInfo.isCanceled) { this.setState({ error: errorInfo.error }); this.removePendingPromise(wrappedPromise); } }); } render() { const { data, error } = this.state; if (error) { return ( <div className="error"> There was an error fetching data: {error} </div> ); } return ( <div className="data"> <button onClick={this.handleClick}>reload data!</button> <ul className="data-list"> {data.map((item, i) => <li key={i}>{item}</li>)} </ul> </div> ); }}
这样就保证了setState 的时候, 上下文是完整的, 进而从根本上解决报错的问题。
结语
上面介绍了解决warning几种方式,最好的办法当然是用cancelablePromise来处理
, 但是这种方式有一定的侵入性,带来了额外的开发成本。 如果你实在不能忍受那个报错, 可以使用这种方式, 当然也可以选择无视它
。
具体如何选择还需要各位看官老爷自行斟酌。
以上, 希望对大家有所启发, 谢谢。
参考资料:
https://reactjs.org/blog/2015...
https://github.com/facebook/r...