setState函数的异步性

简述

在某些情况下,React框架出于性能优化考虑,可能会将多次state更新合并成一次更新。正因为如此,setState实际上是一个异步的函数。 但是,有一些行为也会阻止React框架本身对于多次state更新的合并,从而让state的更新变得同步化。 比如: eventListeners, Ajax, setTimeout 等等。

详解

当setState() 函数执行的时候,函数会创建一个暂态的state作为过渡state,而不是立即修改this.state。 如果在调用setState()函数之后尝试去访问this.state,你得到的可能还是setState()函数执行之前的结果。 在使用setState()的情况下,看起来同步执行的代码其实执行顺序是得不到保证的。原因上面也提到过,React可能会将多次state更新合并成一次更新来优化性能。

运行下面这段代码,你会发现当和addEventListener, setTimeout 函数或者发出AJAX call的时候,调用setState, state会发生改变。并且render函数会在setState()函数被触发之后马上被调用。那么到底发生了什么呢?事实上,类似setTimeout()函数或者发出ajax call的fetch函数属于调用浏览器层面的API,这些函数的执行并不存在与React的上下文中,所以React并不能够像控制其他存在与其上下文中的函数一样,将多次state更新合并成一次。

在上面这些例子中,React框架之所以在选择在调用setState函数之后立即更新state而不是采用框架默认的方式,即合并多次state更新为一次更新,是因为这些函数调用(fetch,setTimeout等浏览器层面的API调用)并不处于React框架的上下文中,React没有办法对其进行控制。React在此时采用的策略就是及时更新,确保在这些函数执行之后的其他代码能拿到正确的数据(即更新过的state)。

class TestComponent extends React.Component {

  constructor(props) {
    super(props);
    this.state = {
      dollars: 10
    }
    this.onMouseLeaveHandler = this.onMouseLeaveHandler.bind(this);
    this.onTimeoutHandler = this.onTimeoutHandler.bind(this);
    this.onAjaxCallback = this.onAjaxCallback.bind(this);
    this.onClickHandler = this.onClickHandler.bind(this);
  }

  componentDidMount() {
    // Add custom event via `addEventListener`
    //
    // The list of supported React events does include `mouseleave`
    // via `onMouseLeave` prop
    //
    // However, we are not adding the event the `React way` - this will have
    // effects on how state mutates
    //
    // Check the list here - https://facebook.github.io/react/docs/events.html
    document.getElementById('testButton').addEventListener('mouseleave', this.onMouseLeaveHandler);

    // Add JS timeout
    //
    // Again,outside React `world` - this will also have effects on how state
    // mutates
    setTimeout(this.onTimeoutHandler, 10000);

    // Make AJAX request
    fetch('https://api.github.com/users')
      .then(this.onAjaxCallback);
  }

  onClickHandler = () => {
    console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 10
    });
    console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
  }

  onMouseLeaveHandler = () => {
    console.log('State before (mouseleave): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 20
    });
    console.log('State after (mouseleave): ' + JSON.stringify(this.state));
  }

  onTimeoutHandler = () => {
    console.log('State before (timeout): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 30
    });
    console.log('State after (timeout): ' + JSON.stringify(this.state));
  }

  onAjaxCallback = (err, res) => {
    if (err) {
      console.log('Error in AJAX call: ' + JSON.stringify(err));
      return;
    }

    console.log('State before (AJAX call): ' + JSON.stringify(this.state));
    this.setState({
      dollars: this.state.dollars + 40
    });
    console.log('State after (AJAX call): ' + JSON.stringify(this.state));
  }

  render() {
    console.log('State in render: ' + JSON.stringify(this.state));

    return (
       <button
         id="testButton"
         onClick={this.onClickHandler}>
         'Click me'
      </button>
    );
  }
}

ReactDOM.render(
  <TestComponent />,
  document.getElementById('app')
);

解决setState函数异步的办法?

根据React官方文档,setState函数实际上接收两个参数,其中第二个参数类型是一个函数,作为setState函数执行后的回调。通过传入回调函数的方式,React可以保证传入的回调函数一定是在setState成功更新this.state之后再执行。

例子

_onClickHandler: function _onClickHandler() {
   console.log('State before (_onClickHandler): ' + JSON.stringify(this.state));
   this.setState({
   dollars: this.state.dollars + 10
   }, () => {
   console.log('Here state will always be updated to latest version!');
   console.log('State after (_onClickHandler): ' + JSON.stringify(this.state));
   });
}

更多关于setState的小知识

其实setState作为一个函数,本身是同步的。只是因为在setState的内部实现中,使用了React updater的enqueueState 或者 enqueueCallback方法,才造成了异步。

下面这段是React源码中setState的实现:

ReactComponent.prototype.setState = function(partialState, callback) {
  invariant(
    typeof partialState === 'object' ||
    typeof partialState === 'function' ||
    partialState == null,
    'setState(...): takes an object of state variables to update or a ' +
    'function which returns an object of state variables.'
  );
  this.updater.enqueueSetState(this, partialState);
  if (callback) {
    this.updater.enqueueCallback(this, callback, 'setState');
  }
};

而updater的这两个方法,又和React底层的Virtual Dom(虚拟DOM树)的diff算法有紧密的关系,所以真正决定同步还是异步的其实是Virtual DOM的diff算法。

参考资料:

results matching ""

    No results matching ""