React的setState

324 阅读5分钟

ReactComponent 类的 setState方法

如下所示,setState方法是定义在 ReactComponent 类中的。

    ReactComponent.prototype.setState = function (partialState, callback) {
      this.updater.enqueueSetState(this, partialState);
      if (callback) {
        this.updater.enqueueCallback(this, callback);
      }
    };

我们平常使用setState方法是这样的:

    this.setState({},[callback])

首先,要如上这样的使用 setState方法就需要继承 ReactComponent类。所以,一般的React组件是这么定义的:

    const App extends React.Component

React.Component 指代的就是 ReactComponent类。 因此新组件是继承了ReactComponent类。

上面的内容让我们知道了setState的来源,接下来我们就来看看setState是如何发挥作用的。

setState 做了什么?

setState 方法的作用很简单虎牙如下两个行核心代码。

    this.updater.enqueueSetState(this, partialState);
    if (callback) {
        this.updater.enqueueCallback(this, callback);
    }

this.updater

this.updater 是在组件实例化的时候赋予的。

ReactCompositeComponent 的 mountComponent 方法中,实例化了组件,并为其赋值:inst.updater = ReactUpdateQueue;

所以,this.updater 是 ReactUpdateQueue 模块。

this.updater.enqueueSetState

这里有必要说一下this这个值,this指代的就是当前组件的一个实例。

看一下 enqueueSetState 方法。

    enqueueSetState: function (publicInstance, partialState) {
        var internalInstance = getInternalInstanceReadyForUpdate(publicInstance, 'setState');
    
        if (!internalInstance) {
          return;
        }
        
        '
            这里,如之前已经调用过replaceState的话,_pendingStateQueue就应该是有值的。否则就是没有值。
            这里会将多个setState的值放在一个数组里 也就是为 _pendingStateQueue 属性赋值为,某次事件的全部 setState方法
        '
        var queue = internalInstance._pendingStateQueue || (internalInstance._pendingStateQueue = []);
        queue.push(partialState);
        
        
        '
            调用 enqueueUpdate 方法,往下我们来看看 enqueueUpdate 方法
        '
        enqueueUpdate(internalInstance);
    },
getInternalInstanceReadyForUpdate

getInternalInstanceReadyForUpdate 方法就是返回 组件的挂载实例。 也就是 instantiateReactComponent 方法 实例化的 组件。

    function getInternalInstanceReadyForUpdate(publicInstance, callerName) {
        
        var internalInstance = ReactInstanceMap.get(publicInstance);
        return internalInstance;
    }

enqueueUpdate

    function enqueueUpdate(internalInstance) {
        ReactUpdates.enqueueUpdate(internalInstance);
    }

这个方法在 根组件挂载的文章中说过,我们这里再来看看

    function enqueueUpdate(component) {
        ensureInjected();
    
        if (!batchingStrategy.isBatchingUpdates) {
            batchingStrategy.batchedUpdates(enqueueUpdate, component);
            return;
        }
        
        dirtyComponents.push(component);
    }

这里就是为 dirtyComponents 放置了要更新的组件。

这里请参考文章

最终是调用了挂载实例的 performUpdateIfNecessary 方法来更新组件

performUpdateIfNecessary

performUpdateIfNecessary 方法我们来看看与 state相关的部分如下。

    if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(transaction, this._currentElement, this._currentElement, this._context, this._context);
    }

如果 _pendingStateQueue 存在的haunt就调用 方法 this.updateComponent,也就是挂载实例的 updateComponent 方法。

关于该方法 请参考这篇文章 我们这里挑出关于state的部分来看看。

_processPendingState 方法

该方法来将多个setState 处理下:

    _processPendingState: function (props, context) {
       
        '
            这里 queue 是要处理的state的队列。
            nextState是初始情况下的 state信息。replace表示是否调用了replaceState方法。
            这里生成了初始的 state
        '
        var nextState = assign({}, replace ? queue[0] : inst.state);
       
        '
            然后呢就遍历一下queue队列,拿出每一个 要改变的state,然后合并这些state。核心方法是 Assign方法。
            简单的来说就是 将一个对象合并到另一个对象上。
            如此,即使你调用一百次setState方法,最后,组件只会更新一次。
            
        '
        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;
      },

React组件中调用 setState 的时候,React会将多个state进行合并处理得到最后的state,而后只更新一次state来提升性能。融合state的过程其实简单的来说就是合并几个对象的过程。

setState的同步与异步的问题

如此,我们使用 this.handleState方法调用setState来修改count,和使用 setTimeout 来修改count。

    this.state = {
      count: 1
    };
    
    handleClick() {
        this.handleState();
        this.handleState();
        this.handleState();
        
        let self = this;
        
        setTimeout(function() {
          self.setState({
            count: 12
          });
        }, 10000);
        
        setTimeout(function() {
          self.setState({
            count: 15
          });
        }, 11000);
        
        setTimeout(function() {
          self.setState({
            count: 18
          });
        }, 12000);
    }
    handleState() {
        this.setState(
            {
              count: this.state.count + 1
            },
            () => {
              console.log('asdasd')
            }
        );
    }

虽然我们调用了三次 this.handleState 方法,但是,State最终只修改了一次,这里有一个合并state的过程。

而三次 setTimeout则是确确实实的执行了三次,state在页面上变更了三次。

出现上述状况的原因如下:

batchingStrategy.isBatchingUpdates 的值:

如果 batchingStrategy.isBatchingUpdates 的值为true的话,则会进行批处理,而如果batchingStrategy.isBatchingUpdates值为false的话,则不会采用批处理。

第一

点击事件触发 dispatchEvent(这里参考ReactEventListener模块的dispatchEvent方法) 方法,而后分发了绑定的事件。isBatchingUpdates 的值变为true( 这里使用了 ReactDefaultBatchingStrategy 模块的 批处理策略 )。

批处理事务开始执行( ReactDefaultBatchingStrategyTransaction)。

而后 执行了三个 this.handleState 方法,而后 三个setTimeout方法会等待执行(也就是Event Loop)

第二

三个 this.handleState 方法调用的时候调用了setState方法。而后就是将state一个个的入队也就是 ReactUpdateQueue.enqueueSetState 方法,来为 组件挂载实例的 _pendingStateQueue 赋值(这些值是三个setState方法的state)

第三

这个时候三个 this.handleState 执行完毕,组件被放置在dirtyComponent 队列里,批处理事务开始执行 close方法。

批处理事务在执行 close方法的时候开始了组件更新机制。遍历 dirtyComponent 里的每一项,而后对其进行 performUpdateIfNecessary 选择性更新。

在这个过程中state 会被合并。

第四

上述的这些执行完毕之后,组件以及更新了。 三个 setTimeout 方法 依次调用。这时候 batchingStrategy.isBatchingUpdates 的值是 false。也就是当前没有采用批处理策略。

然后在 ReactUpdate.enqueueUpdate 方法中,开始开启使用批处理模式(这里因为没有批处理,所以又开启了批处理,当然,开启的批处理是针对setTimeout方法的内部的所有操作state的方法而言的。而之前的没有使用批处理是针对setTimeout极其外部的那些内容而言的。批处理会使用批处理事务来处理。),然后组件挂载实例就被放置在 dirtyComponents中。

那挂载实例会被放在 dirtyComponents中,然后,批处理事务就执行dirtyComponent,更新组件。 以此类推,另外的setTimeout方法也是如此。

总结

因为事件触发的时候,React会一开始就默认使用批处理来处理事件内部的代码。于是,这个时候多个setState方法造成的state修改会被合并。

setTimeout和setInterval 则是跳过了当前事件流,在事件内的同步代码执行完毕之后(批处理事务 完成了组件的更新,而且将 isBatchinUpdate 置为false)setTimeout 方法才会开始执行。

setTimeout方法执行的时候,才会主动的采用批处理策略,这个批处理策略是针对 setTimeout方法的内部代码的。所以说,一个setTimeout方法内即使有多个setState方法,也只会更新一次组件。

如上,便是setState方法可以同步或者是异步执行的秘密。

能够造成setState同步执行的还有使用DOM2级事件流来添加事件(addEventListener、attachEvent)