React-setState

349 阅读4分钟

setState是同步还是异步

回想在使用react的setState时,setState操作完成之后,可以立马拿到结果吗?
在合成事件、react的生命周期函数是不可以立马拿到结果的,😯,那setState是异步的;
不对,但在setTimeOut和原生事件中,是可以立马拿到结果的,setState是同步的;
😊,那setState到底是同步的还是异步的呢?其实setState有时的表现是同步,有时的表现是异步

React15中setState具体实现流程

  • enququUpdate部分源码
function enqueueUpdate(component) {
  // 如果没有处于批量创建/更新组件的阶段,则处理update state事务
  if (!batchingStrategy.isBatchingUpdates) {
    batchingStrategy.batchedUpdates(enqueueUpdate, component);
    return;
  }
  // 如果正处于批量创建/更新组件的过程,将当前的组件放在dirtyComponents数组中
  dirtyComponents.push(component);
}

同步与异步的区别是通过改变isBatchingUpdates的布尔值来实现的;

React16中setState具体实现,结合具体场景

setState异步的场景有哪些

合成事件中的setState

合成事件 — React为了解决事件的跨平台性和兼容性,对原生的事件机制进行了封装,形成了合成事件,比如下面一段代码

    class App extends React.Component {
      state = { val: 0 }
    
      increment = () => {
        this.setState({ val: this.state.val + 1 })
        console.log(this.state.val) // 输出的是更新前的val --> 0
      }
      render() {
        return (
          <div onClick={this.increment}>
            {`Counter is: ${this.state.val}`}
          </div>
        )
      }
    }

上面合成事件中,源码执行的流程

如图,在执行合成事件逻辑后,可以得到isRendering= false、isBatchingUpdates=true、isUnbatchingUpdates=false,进入requestWork函数

function requestWork(root, 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, false);
    }
    return;
  }

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

由代码可知,合成事件后的会进入第二个分支,会直接return;最终回到了interactiveUpdates这个方法,这个方法中有个try finally语法,会先执行try代码块中的语句,再执行finally中的代码;

function interactiveUpdates$1(fn, a, b) {
  if (isBatchingInteractiveUpdates) {
    return fn(a, b);
  }
  // If there are any pending interactive updates, synchronously flush them.
  // This needs to happen before we read any handlers, because the effect of
  // the previous event may influence which handlers are called during
  // this event.
  if (!isBatchingUpdates && !isRendering && lowestPendingInteractiveExpirationTime !== NoWork) {
    // Synchronously flush pending interactive updates.
    performWork(lowestPendingInteractiveExpirationTime, false, null);
    lowestPendingInteractiveExpirationTime = NoWork;
  }
  var previousIsBatchingInteractiveUpdates = isBatchingInteractiveUpdates;
  var previousIsBatchingUpdates = isBatchingUpdates;
  isBatchingInteractiveUpdates = true;
  isBatchingUpdates = true;  // 把requestWork中的isBatchingUpdates标识改为true
  try {
    return fn(a, b);
  } finally {
    isBatchingInteractiveUpdates = previousIsBatchingInteractiveUpdates;
    isBatchingUpdates = previousIsBatchingUpdates;
    if (!isBatchingUpdates && !isRendering) {
      performSyncWork();
    }
  }
}

所以当在increment中调用setState之后去console.log时,是属于try代码块中执行,并没有完成state的更新,所以输入的结果还是原来的state值;当在try代码块执行结束,即increment函数执行结束,就会执行finally中的代码,执行了performSyncWork,这时候会更新state并且渲染到UI上;

React生命周期函数中的setState
class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    this.setState({ val: this.state.val + 1 })
   console.log(this.state.val) // 输出的还是更新前的值 --> 0
 }
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

在生命周期函数componentDidMount中,isRending=true,会直接return,所以并不会立马更新,会有异步的效果;

setState同步的场景有哪些

原生事件中的setState
class App extends Component {

  state = { val: 0 }

  changeValue = () => {
    this.setState({ val: this.state.val + 1 })
    console.log(this.state.val) // 输出的是更新后的值 --> 1
  }

 componentDidMount() {
    document.body.addEventListener('click', this.changeValue, false)
 }
 
  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

原生事件中,没有react合成事件的执行流程,在执行到requestWork时,因为 expirationzTime === Syn 的条件,会直接执行performSyncWork去更新,所以能够立马输出结果,是同步的;

setTimeOut中的setState
class App extends Component {

  state = { val: 0 }

 componentDidMount() {
    setTimeout(_ => {
      this.setState({ val: this.state.val + 1 })
      console.log(this.state.val) // 输出更新后的值 --> 1
    }, 0)
 }

  render() {
    return (
      <div>
        {`Counter is: ${this.state.val}`}
      </div>
    )
  }
}

所有setTimeOut中的setState都可以同步的拿到值,因为setTimeOut属于宏任务;比如之前的合成事件,由于你是 setTimeout(_ => { this.setState()}, 0) 是在 try 代码块中,当你 try 代码块执行到 setTimeout 的时候,把它丢到列队里,并没有去执行,而是先执行的 finally 代码块,等 finally 执行完了, isBatchingUpdates 又变为了 false ,导致最后去执行队列里的 setState 时候, requestWork 走的是和原生事件一样的 expirationTime === Sync if分支,所以表现就会和原生事件一样,可以同步拿到最新的state值。

总结

  • setState 只在合成事件和钩子函数中是“异步”的,在原生事件和 setTimeout 中都是同步的。
  • setState的“异步”并不是说内部由异步代码实现,其实本身执行的过程和代码都是同步的,只是合成事件和钩子函数的调用顺序在更新之前,导致在合成事件和钩子函数中没法立马拿到更新后的值,形式了所谓的“异步”,当然可以通过第二个参数 setState(partialState, callback) 中的callback拿到更新后的结果。
  • setState 的批量更新优化也是建立在“异步”(合成事件、钩子函数)之上的,在原生事件和setTimeout 中不会批量更新,在“异步”中如果对同一个值进行多次 setState , setState 的批量更新策略会对其进行覆盖,取最后一次的执行,如果是同时 setState 多个不同的值,在更新时会对其进行合并批量更新。

😔第一篇博客,感觉还是参考的很多,慢慢学习吧!

参考学习链接