商店的变更监听器没有在 componentWillUnmount 上被删除?
Posted
技术标签:
【中文标题】商店的变更监听器没有在 componentWillUnmount 上被删除?【英文标题】:Stores' change listeners not getting removed on componentWillUnmount? 【发布时间】:2015-11-14 06:31:26 【问题描述】:我正在 reactjs-flux 上编写一个简单的应用程序,一切正常,除了我收到来自 reactjs 的警告,告诉我我正在未安装的组件上调用 setState。
我发现这是因为没有从 componentWillUnmount
上的存储中删除组件所连接的 changelisteners。我知道这一点是因为当我从Eventemitter
打印侦听器列表时,我看到应该被销毁的侦听器仍然存在,并且随着我多次安装/卸载相同的组件,列表变得更大。
我从我的 BaseStore 粘贴代码:
import Constants from '../core/Constants';
import EventEmitter from 'events';
class BaseStore extends EventEmitter
// Allow Controller-View to register itself with store
addChangeListener(callback)
this.on(Constants.CHANGE_EVENT, callback);
removeChangeListener(callback)
this.removeListener(Constants.CHANGE_EVENT, callback);
// triggers change listener above, firing controller-view callback
emitChange()
this.emit(Constants.CHANGE_EVENT);
export default BaseStore;
我从遇到此错误的组件中粘贴了相关代码(尽管所有组件都会发生这种情况):
@AuthenticatedComponent
class ProductsPage extends React.Component
static propTypes =
accessToken: PropTypes.string
;
constructor()
super();
this._productBatch;
this._productBatchesNum;
this._activeProductBatch;
this._productBlacklist;
this._searchById;
this._searchingById;
this.state = this._getStateFromStore();
componentDidMount()
ProductsStore.addChangeListener(this._onChange.bind(this));
componentWillUnmount()
ProductsStore.removeChangeListener(this._onChange.bind(this));
_onChange()
this.setState(this._getStateFromStore());
在这一点上,这让我非常抓狂。有什么想法吗?
谢谢!
【问题讨论】:
你确定componentWillUnmount()
被解雇了吗?
是的,我在所有组件的所有 componentWillUnmount
上放置了 console.logs,它们正在被解雇。
【参考方案1】:
尝试从您的addChangeListener
和removeChangeListener
中删除.bind(this)
。它们在被调用时已经绑定到您的组件。
【讨论】:
不,因为onchange
在productsstore
方法中被调用,this
的值是指productstore
对象。我已经通过打印带有和不带有bind(this)
的this
的值来确认这一点。
嗨,自从上次我偶然发现这个错误几次,我找到了以下解决方案:componentWillUnmount() this.isUnmounted = true;
并且,从现在开始,像这样检查每个 setState 请求:if (!this.isUnmounted) this.setState( isLoading: false, isDisabled: false );
.这来自这篇文章中的 cmets,顺便说一句:jaketrent.com/post/set-state-in-callbacks-in-react【参考方案2】:
所以我找到了解决方案,结果我只需将this._onChange.bind(this)
分配给一个内部属性,然后将其作为参数传递给removechangelistener
和addchangelistener
。这是解决方案:
componentDidMount()
this.changeListener = this._onChange.bind(this);
ProductsStore.addChangeListener(this.changeListener);
this._showProducts();
componentWillUnmount()
ProductsStore.removeChangeListener(this.changeListener);
但是,我不知道为什么这可以解决问题。有什么想法吗?
【讨论】:
我面临同样的问题。不过,您的解决方案并没有为我解决问题……令人沮丧。它不会发生在其他组件上。 嗨,自从上次我偶然发现这个错误几次,我找到了以下解决方案:componentWillUnmount() this.isUnmounted = true;并且,从现在开始,像这样检查每个 setState 请求: if (!this.isUnmounted) this.setState( isLoading: false, isDisabled: false ); 。这来自这篇文章中的 cmets,顺便说一句:jaketrent.com/post/set-state-in-callbacks-in-react 你好杰拉德。我最终使用了其他东西。我使用了一个名为eventemitter3
的库,而不是 node 的 events
。这个库为你给它的每个回调附加一个“this”,并且你也在组件的卸载函数上给它同样的“this”。因为回调与您的“this”相关联,所以正确的回调函数是“未侦听”,在节点的events
上,它与您取消注册的函数不匹配,因此会继续侦听。【参考方案3】:
Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op. Please check the code for the exports component.
我在多个 React 组件中使用完全相同的实现。即,这在多个 .jsx 组件中重复。
componentDidMount: function()
console.log('DidMount- Component 1');
ViewStateStore.addChangeListener(this._onChange);
,
componentWillUnmount: function()
console.log('DidUnMount- Component 1');
ViewStateStore.removeChangeListener(this._onChange);
,
_onChange:function()
console.log('SetState- Component 1');
this.setState(getStateFromStores());
,
可能的解决方案
目前以下对我来说是可行的,但它有点喜怒无常。将回调包装在函数/命名函数中。
ViewStateStore.addChangeListener(function ()this._onChange);
大家也可以试试
ViewStateStore.addChangeListener(function named()this._onChange);
理论
EventEmitter 出于某种原因在识别要删除的回调时感到困惑。使用命名函数可能对此有所帮助。
【讨论】:
实际上与当前答案相同,只是实现方式略有不同。【参考方案4】:我就这么决定了
class Tooltip extends React.Component
constructor (props)
super(props);
this.state =
handleOutsideClick: this.handleOutsideClick.bind(this)
;
componentDidMount ()
window.addEventListener('click', this.state.handleOutsideClick);
componentWillUnmount ()
window.removeEventListener('click', this.state.handleOutsideClick);
【讨论】:
【参考方案5】:这是一个 es6 问题。 React.createClass 为其作用域内定义的所有函数正确绑定“this”。
对于 es6,你必须自己做一些事情来绑定正确的 'this'。但是,调用 bind(this) 每次都会创建一个新函数,并将其返回值传递给 removeChangeListener 将与传递给由较早的 bind(this) 调用创建的 addChangeListener 的函数不匹配。
我在这里看到了一种解决方案,其中对每个函数调用一次 bind(this),然后保存返回值并在以后重新使用。那会很好的。一个更流行且更简洁的解决方案是使用 es6 的箭头函数。
componentDidMount()
ProductsStore.addChangeListener(() => this._onChange() );
componentWillUnmount()
ProductsStore.removeChangeListener(() => this._onChange());
箭头函数捕获封闭上下文的“this”,而无需每次都创建新函数。它是为这样的东西设计的。
【讨论】:
这也行不通 - 您正在删除与添加的侦听器不同的侦听器【参考方案6】:短版:expect(f.bind(this)).not.toBe(f.bind(this));
更长的解释:
问题的原因是EventEmitter.removeListener
要求您传递之前在EventEmitter.addListener
注册的函数。如果您传递对任何其他函数的引用,则它是静默无操作。
在您的代码中,您将 this._onChange.bind(this)
传递给 addListener。 bind
返回一个绑定到 this 的 new 函数。然后,您将丢弃对该绑定函数的引用。然后,您尝试删除由绑定调用创建的另一个 new 函数,这是一个无操作,因为从未添加过。
React.createClass 自动绑定方法。在 ES6 中,你需要在你的构造函数中手动绑定:
@AuthenticatedComponent
class ProductsPage extends React.Component
static propTypes =
accessToken: PropTypes.string
;
constructor()
super();
this._productBatch;
this._productBatchesNum;
this._activeProductBatch;
this._productBlacklist;
this._searchById;
this._searchingById;
this.state = this._getStateFromStore();
// Bind listeners (you can write an autoBind(this);
this._onChange = this._onChange.bind(this);
componentDidMount()
// listener pre-bound into a fixed function reference. Add it
ProductsStore.addChangeListener(this._onChange);
componentWillUnmount()
// Remove same function reference that was added
ProductsStore.removeChangeListener(this._onChange);
_onChange()
this.setState(this._getStateFromStore());
有多种方法可以简化绑定——你可以使用 ES7 的 @autobind
方法装饰器(例如 npm 上的 autobind-decorator),或者编写一个 autoBind 函数,在构造函数中调用 autoBind(this);
。
在 ES7 中,您将(希望)能够使用类属性来获得更方便的语法。如果您愿意作为第一阶段提案 http://babeljs.io/docs/plugins/transform-class-properties/ 的一部分,您可以在 Babel 中启用此功能。然后,您只需将事件侦听器方法声明为类属性而不是方法:
_onChange = () =>
this.setState(this._getStateFromStore());
因为 _onChange 的初始化程序是在构造函数的上下文中调用的,所以箭头函数会自动将 this
绑定到类实例,因此您可以将 this._onChange
作为事件处理程序传递,而无需手动绑定它。
【讨论】:
该技术应该可以正常工作。您是否已对其进行调试以查看发生了什么?或者您可以发布一个指向您的代码的链接吗? 已经很久了,但是 TomW 绝对就在这里 ;) 非常适合我。但我不明白为什么它适用于没有这个解决方案的其他商店,但对于一家商店我真的需要这个。奇怪的行为......【参考方案7】:由于您已经知道解决方案here,我将尝试解释发生了什么。 根据 ES5 标准,我们曾经编写以下代码来添加和删除监听器。
componentWillMount: function()
BaseStore.addChangeListener("ON_API_SUCCESS", this._updateStore);
,
componentWillUnmount: function()
BaseStore.removeChangeListener("ON_API_SUCCESS", this._updateStore);
在上面的代码中,回调函数的内存引用(即:this._updateStore)是相同的。因此,removeChangeListener 将寻找参考并将其删除。
由于 ES6 标准缺少自动绑定 this
,默认情况下您必须将 this
显式绑定到函数。
Note: Bind method returns new reference for the callback.
有关绑定的更多信息,请参阅here
这就是问题发生的地方。当我们执行this._updateStore.bind(this)
时,bind 方法返回该函数的新引用。因此,您作为参数发送给 addChangeListener 的引用与 removeChangeListener 方法中的引用不同。
this._updateStore.bind(this) != this._updateStore.bind(this)
解决方案:
有两种方法可以解决这个问题。
1.将事件处理程序(ie: this._updateStore)
作为成员变量存储在构造函数中。 (您的解决方案)
2. 在 store 中创建一个自定义 changeListener 函数,它将为您绑定this
。 (来源:here)
方案一说明:
constructor (props)
super(props);
/* Here we are binding "this" to _updateStore and storing
that inside _updateStoreHandler member */
this._updateStoreHandler = this._updateStore.bind(this);
/* Now we gonna user _updateStoreHandler's reference for
adding and removing change listener */
this.state =
data: []
;
componentWillMount ()
/* Here we are using member "_updateStoreHandler" to add listener */
BaseStore.addChangeListener("ON_STORE_UPDATE", this._updateStoreHandler);
componentWillUnmount ()
/* Here we are using member "_updateStoreHandler" to remove listener */
BaseStore.removeChangeListener("ON_STORE_UPDATE", this._updateStoreHandler);
在上面的代码中,我们将 this
绑定到 _updateStore 函数并将其分配给构造函数内部的成员。稍后我们将使用该成员来添加和删除更改侦听器。
方案二解释:
在这个方法中,我们修改了 BaseStore 的功能。想法是修改 BaseStore 中的 addChangeListener 函数以接收第二个参数 this
并在该函数内将 this
绑定到回调并存储该引用,以便在删除更改侦听器时我们可以使用该引用删除。
您可以找到完整的代码要点here 和源代码here。
【讨论】:
以上是关于商店的变更监听器没有在 componentWillUnmount 上被删除?的主要内容,如果未能解决你的问题,请参考以下文章