对React setState的一些思考与心得

563 阅读7分钟
原文链接: www.jianshu.com

前言

这篇文章主要是为了纪录一些自己对于setState的认识的不断深入的过程。

第一阶段 初识setState

使用过React的应该都知道,在React中,一个组件中要读取当前状态需要访问this.state,但是更新状态却需要使用用this.setState,不是直接在this.state上修改,就比如这样:

//读取状态
const count = this.state.count;

//更新状态
this.setState({count: count + 1});

//无意义的修改
this.state.count = count + 1;

其实这主要有几点考虑,首先this.state说到底只是一个对象,单纯的去修改一个对象的值是毫无意义的,在React中只有去驱动UI的更新才会有意义,因此虽然我们可以尝试直接改变this.state,但并没有驱动UI的重新渲染,因此这种操作也就毫无意义。也正是由于这个原因,我们就需要使用this.setState来驱动组件的更新过程。

然后在我刚学习React时,我就看见了这段很经典的代码:

function incrementMultiple() {
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
  this.setState({count: this.state.count + 1});
}

作为一名JSer,我看完就毫不犹豫的想到,这特么不就是count的值加3么,但转眼看了下面的答案,光速打脸,实际的结果是state只增加了1。然后我就不由想到当时没怎么看懂的React文档中的一些话:状态更新可能是异步的,状态更新合并。恩,没毛病,因为异步且会合并,因此这三条语句合并为一条语句了,所以就只执行一次。然后就扭头溜了,并没有去思考一些深层次的问题。

第二阶段 setState理解的进阶

但是随着对React的理解的逐步加深,我开始对setState有了更加深的理解:

首先我意识到this.setState会通过引发一次组件的更新过程来引发重新绘制。也就是说setState的调用会引起React的更新生命周期的四个函数的依次调用:

  • shouldComponentUpdate
  • componentWillUpdate
  • render
  • componentDidUpdate

我们都知道,在React生命周期函数里,以render函数为界,无论是挂载过程和更新过程,在render之前的几个生命周期函数,this.state和Props都是不会发生更新的,直到render函数执行完毕后,this.state才会得到更新。(有一个例外:当shouldComponentUpdate函数返回false,这时候更新过程就被中断了,render函数也不会被调用了,这时候React不会放弃掉对this.state的更新的,所以虽然不调用render,依然会更新this.state。)

React的官方文档有提到过这么一句话:

状态更新会合并(也就是说多次setstate函数调用产生的效果会合并)。

起初我对这句话理解并不是很深刻,但按照官方文档的代码示例写了这么一段代码:

function updateName() {
  this.setState({Age: '22'})
  this.setState({Name: 'srtian'})
}

果然执行结果与以下代码是等价的

function updateName() {
  this.setState({Age: '22', Name: 'srtian})
}

于是我将其理解为一个队列,每个this.setState()都会被合并起来,排成一排,到最后一次解决。但对其设计的原因并不理解,只知道这样有利于性能(也是在文档上看到的)。

直到理解上面React生命周期函数的原理后,我才理解了setState关于这个设计的意图。

前面我们提到过,每一次使用setState都会调用一次更新的生命周期,如果每一次this.serState()都调用一次上面那四个生命周期函数,虽然以上四个函数都是纯函数,性能浪费上还好,但render函数会将结果拿去做Virtual DOM比较和更新DOM树,这个就比较费时间。因此,将多个this.setSate进行合并,render函数就能够将合并后的this.setState()的结果一次性的与Virtual DOM比较然后更新DOM树,这样就能够用有效的提升性能。

除此之外,我还认为setState的设计十分巧妙,一般来说只在render函数后才会进行更新this.state。这其实也避免了React16的Fiber可能会产生的一个问题:由于Fiber下的组件更新是可以中断,也就是说在一个组件的更新过程中,可能更新到一半的时候就由于其他原因而中断更新,回去做更重要的事情了,在做完更重要的事情后,再回来更新这个组件,这会导致前面的那些生命周期函数可能会执行多次。因此如果在render之前this.setState()就改变状态的话,很有可能就会导致组件状态的多次更新,从而导致组件状态的混乱。

第三阶段 从源码理解setstate

经历了上面那个阶段,我算是对setState有那么一些理解了,但还是不能理解很多东西比如:this.setState()的是怎么合并的?setState()到底是怎样一种骚操作?...等等。然后我又看见了这段经典的代码:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

恩!按照我多年经验,这波操作我看不懂!

image

于是硬着头皮打开了React源码,开始一波瞎分析: 首先就是setState了,可以看出它接受两个参数partialState和callback,其中partialState顾名思义就是部分state,起这个名字也能就是想表达它的state没有改变(瞎猜的。。。)。以下是省略了一部分的代码,只看核心部分。

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');
  }
};
 enqueueSetState: function(publicInstance, partialState) {
    if (__DEV__) {
      ReactInstrumentation.debugTool.onSetState();
      warning(
        partialState != null,
        'setState(...): You passed an undefined or null state object; ' +
          'instead, use forceUpdate().',
      );
    }

    var internalInstance = getInternalInstanceReadyForUpdate(
      publicInstance,
      'setState',
    );

    if (!internalInstance) {
      return;
    }

    var queue =
      internalInstance._pendingStateQueue ||
      (internalInstance._pendingStateQueue = []);
    queue.push(partialState);

    enqueueUpdate(internalInstance);
  }
// 通过enqueueUpdate执行state更新
function enqueueUpdate(component) {
  ensureInjected();
// 这里是个重点,插眼!!!!!!!!!!!
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  dirtyComponents.push(component);
  
  if (component._updateBatchNumber == null) {
    component._updateBatchNumber = updateBatchNumber + 1;
  }
}
// 对_pendingElement, _pendingStateQueue, _pendingForceUpdate进行判断,_pendingStateQueue由于会对state进行修改,所以不为空,然后会调用updateComponent方法
performUpdateIfNecessary: function(transaction) {
    if (this._pendingElement != null) {
      ReactReconciler.receiveComponent(
        this,
        this._pendingElement,
        transaction,
        this._context,
      );
    } else if (this._pendingStateQueue !== null || this._pendingForceUpdate) {
      this.updateComponent(
        transaction,
        this._currentElement,
        this._currentElement,
        this._context,
        this._context,
      );
    } else {
      this._updateBatchNumber = null;
    }
  },

然后就看了updateComponent方法:

{   // 会检测组件中的state和props是否发生变化,有变化才会进行更新; 
    // 如果shouldUpdateComponent函数中返回false则不会执行组件的更新
    updateComponent: function (transaction,
                               prevParentElement,
                               nextParentElement,
                               prevUnmaskedContext,
                               nextUnmaskedContext,) {
        var inst = this._instance;
        var nextState = this._processPendingState(nextProps, nextContext);
        var shouldUpdate = true;

        if (!this._pendingForceUpdate) {
            if (inst.shouldComponentUpdate) {
                if (__DEV__) {
                    shouldUpdate = measureLifeCyclePerf(
                            () => inst.shouldComponentUpdate(nextProps, nextState, nextContext),
                            this._debugID,
                            'shouldComponentUpdate',
                    );
                } else {
                    shouldUpdate = inst.shouldComponentUpdate(
                            nextProps,
                            nextState,
                            nextContext,
                    );
                }
            } else {
                if (this._compositeType === CompositeTypes.PureClass) {
                    shouldUpdate =
                            !shallowEqual(prevProps, nextProps) ||
                            !shallowEqual(inst.state, nextState);
                }
            }
        }
        
    },
// 该方法会合并需要更新的state,然后加入到更新队列中
    _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 = Object.assign({}, replace ? queue[0] : inst.state);
        for (var i = replace ? 1 : 0; i < queue.length; i++) {
            var partial = queue[i];
            Object.assign(
                    nextState,
                    typeof partial === 'function'
                            ? partial.call(inst, nextState, props, context)
                            : partial,
            );
        }

        return nextState;
    }
};

发现它会调用shouldComponentUpdate和componentWillUpdate方法,看到这不由理解了一个定律:不要在shouldComponentUpdate和componentWillUpdate中调用setState。如果在这两个生命周期里调用setState,会造成造成循环调用。好了这个我理解了,有往前找到了这个——batchedUpdates:

var ReactDefaultBatchingStrategy = {
  isBatchingUpdates: false,
  batchedUpdates: function(callback, a, b, c, d, e) {
    var alreadyBatchingUpdates = ReactDefaultBatchingStrategy.isBatchingUpdates;
    ReactDefaultBatchingStrategy.isBatchingUpdates = true;
    // The code is written this way to avoid extra allocations
    if (alreadyBatchingUpdates) {
      return callback(a, b, c, d, e);
    } else {
      return transaction.perform(callback, null, a, b, c, d, e);
    }
  },

看到这我总算理解了,当我们调用setState时,最终会通过enqueueUpdate执行state更新,就像上面那样有两种更新的模式,一种是批量更新模式,将组建保存在dirtyComponents;另一种非批量模式,将会遍历dirtyComponents,对每一个dirtyComponents调用updateComponent方法。就像这张图:

流程图

至于批量与非批量模式,会通过ReactDefaultBatchingStrategy中的isBatchingUpdates属性来进行判断。在非批量模式下,会立即应用新的state;而在批量模式下,需要更新state的组件会被push 到dirtyComponents,再执行更新。

所以我们再看前面的那坨代码:

class Example extends React.Component {
  constructor() {
    super();
    this.state = {
      val: 0
    };
  }
  
  componentDidMount() {
    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 1 次 log

    this.setState({val: this.state.val + 1});
    console.log(this.state.val);    // 第 2 次 log

    setTimeout(() => {
      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 3 次 log

      this.setState({val: this.state.val + 1});
      console.log(this.state.val);  // 第 4 次 log
    }, 0);
  }

  render() {
    return null;
  }
};

就不难看出它的答案是 0, 0, 3, 4。

总结起来就是这样:

  • this.setState首先会把state推入pendingState队列中
  • 然后将组件标记为dirty
  • React中有事务的概念,最常见的就是更新事务,如果不在事务中,则会开启一次新的更新事务,更新事务执行的操作就是把组件标记为dirty。
  • 判断是否处于batch update
  • 是的话,保存组建于dirtyComponent中,等事务处理完后再统一更新。
  • 不是的话,开启一次新的更新事务,在标记为dirty之后,开始更新组件。因此当setState执行完毕后,组件就更新完毕了,所以会造成定时器同步更新的情况。