商店的变更监听器没有在 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】:

尝试从您的addChangeListenerremoveChangeListener 中删除.bind(this)。它们在被调用时已经绑定到您的组件。

【讨论】:

不,因为onchangeproductsstore 方法中被调用,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) 分配给一个内部属性,然后将其作为参数传递给removechangelisteneraddchangelistener。这是解决方案:

  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 上被删除?的主要内容,如果未能解决你的问题,请参考以下文章

JDK的一个Bug,监听文件变更要小心了

JDK的一个Bug,监听文件变更要小心了

为控制器中的商店事件设置监听器

C# 监听窗口分辨率/DPI变更

vue中如何监听vuex中的数据变化

如何从 Vuex 商店中删除 Firestore 快照侦听器