setState

使用setState来修改react状态。setState修改了状态之后不会立即更新状态,而是把状态放到一个队列中,如果多个setState都修改了状态,会对这些修改操作进行合并(shallowly merged),然后批量更新状态。如果setState传入的是函数,修改操作会执行之后,把结果合并到最终状态。

备注:setState 会re-render组件,除非shouldComponentUpdate() 返回false. 状态merge的时候是shallowly merged。 默认情况下react只在event handlers中才会进行batch update.根据官方文档/开发者介绍,react 17.x发布之后,会进行统一,都会进行batch update。

1.使用setState,传入对象

引用一个例子,

class Sample extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
      var that=this;
    this.setState({
      count: this.state.count + 1
    });
    console.log(this.state.count);    // 打印 0
    this.setState({
      count: this.state.count + 2
    });
    console.log(this.state.count);    // 打印 0
    setTimeout(function(){
        that.setState({
       count: that.state.count + 1
     });
     console.log(that.state.count);   // 打印 3
     that.setState({
       count: that.state.count + 1
     });
     console.log(that.state.count);   // 打印 4
    }, 0);
    // setTimeout(function(){
    //     that.setState({
    //    count: that.state.count + 1
    //  });
    //  console.log(that.state.count);   // 打印
    // }, 0);
  }
  render() {
    return (
      <h1>{this.state.count}</h1>
    )
  }
}

2.setState 传入函数

上面例子的结果,0 0 是因为setState的时候react 会把多个操作放到一个队列缓存,通过react transaction 来实现,执行批量更新。3是因为settimeout作为一个单独事务来执行,并且开始的两个setState 合并操作,只有最后一个setState起作用了。 如何才能不把两次操作合并呢?可以setState传入函数来解决,最终count的值为3

class RootFun extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
    let that = this;
    this.setState(function(state, props) {
      return {
        count: state.count + 1
      }
    });
    this.setState(function(state, props) {
      return {
        count: state.count + 2
      }
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>   //页面中将打印出3
    )
  }
}

3.setState 第2个参数(用法扩展)

componentDidUpdate 和 setState callback (setState(updater, callback))都可以取得最新状态,推荐使用componentDidUpdate。setState callback 会在setState完成并且会在组件的re-rendered之后执行。

class Sample extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0
    };
  }
  componentDidMount() {
      var that=this;
    this.setState({
      count: this.state.count + 1
    },function(pre){
        console.log(this.state.count);    // 打印 1
    });
  }
  render() {
    return (
      <h1>{this.state.count}</h1>
    )
  }
}

4.setState 相关事件

setState -> shouldComponentUpdate ->componentWillUpdate ->render ->componentDidUpdate

5.setState batch update 批量更新实现原理

react setState 通过react transaction 来实现批量更新操作。下面是transaction 执行过程。transaction 创建的时候需要传入getTransactionWrappers,并且传入需要是个数组集合,集合中每个对象都需要实现initialize和close方法。当transaction实例开始perform(anyMethod)时候,会initializeAll(0) 把所有wrapper中initialize都执行,然后执行anyMethod,最后会调用closeAll(0),把所有wrapper中的close方法都执行。 下面看一下transaction的执行示意图(把源码中的图稍微修改一下):

 * <pre>
 *                       wrappers (injected at creation time)
 *                                      +        +
 *                                      |        |
 *                    +-----------------|--------|--------------+
 *                    |                 v        |              |
 *                    |      +---------------+   |              |
 *                    |   +--|    wrapper1   |---|----+         |
 *                    |   |  +---------------+   v    |         |
 *                    |   |          +-------------+  |         |
 *                    |   |     +----|   wrapper2  |--------+   |
 *                    |   |     |    +-------------+  |     |   |
 *                    |   |     |                     |     |   |
 *                    |                               v     v   | wrapper
 *                    | +---+ +---+   +---------+   +---+ +---+ | invariants
 * perform(anyMethod) | |   | |   |   |         |   |   | |   | | maintained
 * +----------------->|-|---|-|---|-->|anyMethod|---|---|-|---|-|-------->
 *                    | |   ^ |   ^   |         ^   |   ^ |   ^ |
 *                    | |   | |   |   |         |   |   | |   | |
 *                    | v   | v   |   v         |   v   | v   | |
 *                    |  ---   ---     ---------     ---   --- |
 *                    |  initialize                    close    |
 *                    +-----------------------------------------+
 * </pre>

先看一下transaction perform 函数的实现源码。当调用transaction的perform方法时,会先执行this.initializeAll(0),然后执行perform传入的方法,最后执行this.closeAll(0)。transaction的实现就是这么简单。问题的关键就在于getTransactionWrappers 函数,initialize 和close方法都是通过这个函数,在transaction初始化的时候传入的。

 perform: function(method, scope, a, b, c, d, e, f) {
    var errorThrown;
    var ret;
    try {
      this._isInTransaction = true;
      errorThrown = true;
      this.initializeAll(0); //把初始化时 传入的wrappers 数组中的所有initialize都执行
      ret = method.call(scope, a, b, c, d, e, f);//perform 调用时传入需要执行的方法 
      errorThrown = false;
    } finally {
      try {
        if (errorThrown) {
          try {
            this.closeAll(0);//把初始化时 传入的wrappers 数组中的所有close都执行(initialize或perform传入的方法异常时会执行)
          } catch (err) {
          }
        } else {
          this.closeAll(0);//把初始化时 传入的wrappers 数组中的所有close都执行
        }
      } finally {
        this._isInTransaction = false;
      }
    }
    return ret;
  },

下面看一下transaction initializeAll 和 函数的实现源码:

 initializeAll: function(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      try {
        this.wrapperInitData[i] = Transaction.OBSERVED_ERROR;
        this.wrapperInitData[i] = wrapper.initialize ?
          wrapper.initialize.call(this) :
          null;// 执行所有的 initialize 方法
      } finally {
        if (this.wrapperInitData[i] === Transaction.OBSERVED_ERROR) {
          try {
            this.initializeAll(i + 1);//initialize 异常时,执行下一个 initialize 方法
          } catch (err) {
          }
        }
      }
    }
  },
  closeAll: function(startIndex) {
    var transactionWrappers = this.transactionWrappers;
    for (var i = startIndex; i < transactionWrappers.length; i++) {
      var wrapper = transactionWrappers[i];
      var initData = this.wrapperInitData[i];
      var errorThrown;
      try {
        errorThrown = true;
        if (initData !== Transaction.OBSERVED_ERROR && wrapper.close) {
          wrapper.close.call(this, initData);// 执行所有的 close 方法
        }
        errorThrown = false;
      } finally {
        if (errorThrown) {
          try {
            this.closeAll(i + 1);//close 异常时,执行下一个 close 方法
          } catch (e) {
          }
        }
      }
    }
    this.wrapperInitData.length = 0;
  }

transaction 文档https://github.com/facebook/react/blob/401e6f10587b09d4e725763984957cf309dfdc30/src/shared/utils/Transaction.js

setState如何使用transaction 完成batch update?

看一下源码,首先定义需要执行的操作 FLUSH_BATCHED_UPDATES,RESET_BATCHED_UPDATES,通过getTransactionWrappers 接口传入transaction 中。当调用 ReactDefaultBatchingStrategy.batchedUpdates ,如果当前处于isBatchingUpdates=false,会通过transation执行callback,并且在close函数中会先调用flushBatchedUpdates(通过此函数来进行batch update),然后重置状态isBatchingUpdates 的值 ;如果isBatchingUpdates=true,会直接调用callback.

var RESET_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: function() {
    ReactDefaultBatchingStrategy.isBatchingUpdates = false;
  }
};

var FLUSH_BATCHED_UPDATES = {
  initialize: emptyFunction,
  close: ReactUpdates.flushBatchedUpdates.bind(ReactUpdates)
};

var TRANSACTION_WRAPPERS = [FLUSH_BATCHED_UPDATES, RESET_BATCHED_UPDATES];

function ReactDefaultBatchingStrategyTransaction() {
  this.reinitializeTransaction();
}

assign(
  ReactDefaultBatchingStrategyTransaction.prototype,
  Transaction.Mixin,
  {
    getTransactionWrappers: function() {
      return TRANSACTION_WRAPPERS;
    }
  }
);

var transaction = new ReactDefaultBatchingStrategyTransaction();

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function(callback, a, b, c, d) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;

    ReactDefaultBatchingStrategy.isBatchingUpdates = true;

    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      callback(a, b, c, d);
    } else {
      transaction.perform(callback, null, a, b, c, d);
    }
  }
};

react 批量更新就是通过调用 flushBatchedUpdates 来实现,来看下如何实现?在flushBatchedUpdates中,我们会执行另一个事务ReactUpdatesFlushTransaction,并且主函数是runBatchedUpdates,主函数会进入react生命周期,判断是否需要更新。

    if (dirtyComponents.length) {
      var transaction = ReactUpdatesFlushTransaction.getPooled();
      transaction.perform(runBatchedUpdates, null, transaction);
      ReactUpdatesFlushTransaction.release(transaction);
    }

runBatchedUpdates 实现如下,会发现_pendingCallbacks函数放到事务的callbackQueue中缓存起来,为什么这样操作?callback不能立即执行,需要render之后再执行,所以先放到事务的callbackQueue中。

function runBatchedUpdates(transaction) {
  var len = transaction.dirtyComponentsLength;
  dirtyComponents.sort(mountOrderComparator);
  for (var i = 0; i < len; i++) {
    var component = dirtyComponents[i];
    var callbacks = component._pendingCallbacks;
    component._pendingCallbacks = null;

    ReactReconciler.performUpdateIfNecessary(
      component,
      transaction.reconcileTransaction
    );

    if (callbacks) {
      for (var j = 0; j < callbacks.length; j++) {
        transaction.callbackQueue.enqueue(
          callbacks[j],
          component.getPublicInstance()
        );
      }
    }
  }
}

6.setState 合并操作实现原理

通过上面事务我们看到 flushBatchedUpdates 执行批量更新。在flushBatchedUpdates 后续操作中,_processPendingState 会先对修改的state进行合并(shallowly merged)。所以,如果setState传入的是函数,不会对修改操作进行合并,而是会立即执行修改,把最终状态合并到最新状态。这就解释了上面例子中,如果需要进行连续修改状态,可以通过setState传入函数来实现。

  _processPendingState: function(props, context) {
    var inst = this._instance;
    var queue = this._pendingStateQueue;
    var replace = this._pendingReplaceState;
    this._pendingReplaceState = false;
    this._pendingStateQueue = null;

    if (!queue) {
      return inst.state;
    }

    if (replace && queue.length === 1) {
      return queue[0];
    }

    var nextState = assign({}, replace ? queue[0] : inst.state);
    for (var i = replace ? 1 : 0; i < queue.length; i++) {
      var partial = queue[i];
      assign(
        nextState,
        typeof partial === 'function' ?
          partial.call(inst, nextState, props, context) :
          partial
      );//合并修改的状态
    }

    return nextState;
  },

备注:

  1. shouldComponentUpdate 或 componentWillUpdate 方 法 中 调 用 setState , 此 时 this._pendingStateQueue != null,则 performUpdateIfNecessary 方法就会调用 updateComponent 方法进行组件更新, 但 updateComponent 方法又会调用 shouldComponentUpdate 和 componentWill- Update 方法,因此造成循环调用, 使得浏览器内存占满后崩溃

为什么componentWillUpdate 调用setState会死循环,componentDidUpdate 不会?

  1. 初次渲染的时候,render时候会开启事务(batchUpdate) _renderNewRootComponent