存储的更改侦听器未在componentWillUnmount上删除?

时间:2015-08-20 14:39:49

标签: reactjs reactjs-flux flux

我在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());
  }
}

这让我非常疯狂。有什么想法吗?

谢谢!

7 个答案:

答案 0 :(得分:21)

简短版本:expect(f.bind(this)).not.toBe(f.bind(this));

更长的解释:

问题的原因是EventEmitter.removeListener要求您传递之前使用EventEmitter.addListener注册的功能。如果您将引用传递给任何其他函数,则它是一个无声的无操作。

在您的代码中,您将this._onChange.bind(this)传递给addListener。 bind会返回绑定到此的函数。然后,您将丢弃对该绑定函数的引用。然后你尝试删除绑定调用创建的另一个 new 函数,它是一个no op,因为它从未添加过。

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(this);的构造函数中调用的autoBind函数。

在ES7中,您(希望)能够使用类属性来获得更方便的语法。如果您愿意,可以在Babel中启用此功能,作为第1阶段提案http://babeljs.io/docs/plugins/transform-class-properties/的一部分。然后,您只需将事件侦听器方法声明为类属性而不是方法:

_onChange = () => {
    this.setState(this._getStateFromStore());
}

因为_onChange的初始化程序是在构造函数的上下文中调用的,所以箭头函数会自动将this绑定到类实例,因此您只需将this._onChange作为事件处理程序传递,而无需手动绑定它。

答案 1 :(得分:4)

所以我找到了解决方案,事实证明我只需要将this._onChange.bind(this)分配给内部属性,然后再将其作为参数传递给removechangelisteneraddchangelistener。这是解决方案:

  componentDidMount() {
    this.changeListener = this._onChange.bind(this);
    ProductsStore.addChangeListener(this.changeListener);
    this._showProducts();
  }
  componentWillUnmount() {
    ProductsStore.removeChangeListener(this.changeListener);
  }
但是,我不知道为什么这会解决这个问题。有什么想法吗?

答案 2 :(得分:1)

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.

我在多个反应组件中使用完全相同的实现。即在几个.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出于某种原因混淆了识别要删除的回调。使用命名函数可能有助于此。

答案 3 :(得分:0)

尝试从addChangeListenerremoveChangeListener中删除{{1}}。它们在被调用时已经绑定到您的组件。

答案 4 :(得分:0)

我决定如此

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 :(得分:0)

这是一个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 :(得分:0)

由于您已经了解了解决方案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. 有关bind

的详细信息,请参阅here

这是问题发生的地方。当我们执行this._updateStore.bind(this)时,bind方法返回该函数的新引用。因此,您作为参数发送到addChangeListener的引用与removeChangeListener方法中的引用不同。

this._updateStore.bind(this) != this._updateStore.bind(this)

<强>解决方案:
有两种方法可以解决这个问题 1.将事件处理程序(ie: this._updateStore)作为成员变量存储在构造函数中。 (你的解决方案)
2.在商店中创建一个自定义changeListener函数,为您绑定this。 (来源:here

解决方案1解释:

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函数并将其分配给构造函数内的成员。稍后我们使用该成员添加和删除更改侦听器。

解决方案2解释: 在此方法中,我们修改BaseStore功能。想法是修改BaseStore中的addChangeListener函数以接收第二个参数this,并在该函数内部将this绑定到回调并存储该引用,以便在删除更改侦听器时我们可以使用该引用删除。

您可以找到完整的代码gist here和来源here