react源码6 - setState批量更新batchedUp

2020-06-06  本文已影响0人  邵志远

很多人在面试的时候遇到过这样一道题,

 state = {
    count: 0,
  };
  handleClick = () => {
    this.count();
  };
  count = () => {
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>{this.state.count}</button>
      </div>
    );
  }

问:这几个console分别是什么?
很多人都会说setState会合并,所以三个都是0。可能再加个异步的说法。然而我们再看下面这道题。

state = {
    count: 0,
  };
  handleClick = () => {
    setTimeout(() => {
      this.count();
    },0)
  };
  count = () => {
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
    this.setState({count: this.state.count + 1});
    console.log(this.state.count);
  };

  render() {
    return (
      <div>
        <button onClick={this.handleClick}>{this.state.count}</button>
      </div>
    );
  }

this.count外面包了一层setTimeout,那么现在的结果是什么呢?
很出乎意料,加了setTimeout之后结果就变成了

image.png
好像之前的异步合并都不存在了,这是为什么呢。
function requestWork(root: FiberRoot, expirationTime: ExpirationTime) {
  addRootToSchedule(root, expirationTime);
  if (isRendering) {
    // Prevent reentrancy. Remaining work will be scheduled at the end of
    // the currently rendering batch.
    return;
  }

  if (isBatchingUpdates) {
    // Flush work at the end of the batch.
    if (isUnbatchingUpdates) {
      // ...unless we're inside unbatchedUpdates, in which case we should
      // flush it now.
      nextFlushedRoot = root;
      nextFlushedExpirationTime = Sync;
      performWorkOnRoot(root, Sync, true);
    }
    return;
  }

  // TODO: Get rid of Sync and use current time?
  if (expirationTime === Sync) {
    performSyncWork();
  } else {
    scheduleCallbackWithExpirationTime(root, expirationTime);
  }
}

react里的更新都要经过requestWork,执行setState的时候通过debug可以看到三次setState过程isBatchingUpdates都为true,也就是都会return,并不会执行react的调度更新performSyncWork。那为什么会这样呢,我们往前看执行过程,可以找到一个batchedUpdates的方法。

function batchedUpdates<A, R>(fn: (a: A) => R, a: A): R {
  const previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingUpdates = true;
  try {
    return fn(a);
  } finally {
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

可以看到,它会return执行一个函数,这个函数相当于我们上面的handleClick(当然还会经过一系列的事件绑定),然后它会判断!isBatchingUpdates && !isRendering如果正确的话就执行performSyncWork()也就是react的指挥调度更新,正常情况下,它会等三个updates都创建完之后,再触发调度更新。但是在setTimeout的上下文它的执行环境是window,并没有isBatchingUpdates设置true的情况,所以它就会立即执行更新。
那我们想在setTimeout里执行函数函数,也不想多次触发更新该怎么办呢,在react-dom里引用batchedUpdates方法,并使用它就ok了。

 setTimeout(() => {
      batchedUpdated(() => this.count());
 },0)

通过上面的过程,我们可以得到一个结论:

setsetState它本身的方法调用是同步的,但是setState并不标志着react的state立马就更新了,这个更新是要根据我们当前环境的执行上下文来判定的,如果处于批量更新的情况下,那么不会立马更新,反之则有可能会立即更新。(有可能是因为这里还有concurrentMode等异步渲染的情况)
上一篇下一篇

猜你喜欢

热点阅读