📌纯干货!5分钟详解你不得不知的setState同/异步原理🔥

1,787 阅读5分钟

state.png

面试官:"React中的setState是同步的还是异步的?"

我:"在react合成事件和钩子函数中是异步的,在原生事件和setTimeout中都是同步的,哼~小样"

面试官:"为什么setState有些时候是同步,有些时候是异步的?"

我: "......"

​ 我们都知道,setState在react里是一个很重要的方法,使用它可以更新组件状态,但是为什么相同的setState方法在不同地方使用会呈现出不同的表现形式呢?今天,我们从源码的角度带大家探究setState是如何完成的以及为什么会有同步和异步的区别。

setState(updater, callback)方法执行后,react通常会集齐一批需要更新的组件,然后批量更新来保证渲染性能,所以我们在使用setState改变状态后,通常没办法立刻通过this.state拿到最新的状态。

​ 如果你需要拿到最新的状态,可以在componentDidUpdate或者setStatecallback里获取。

合成事件中的setState

合成事件

​ 首先的了解什么是合成事件,react为了解决跨平台兼容性的问题,封装了一套自己的事件机制,代理了浏览器的原生事件,在JSX中常见的onClickonChange这些都是合成事件。

class App extends React.Component {
  state = { count: 0 };

  onClick = () => {
    this.setState((prevState) => ({ count: prevState.count + 1 }));
  };

  render() {
    return <button onClick={this.onClick} />;
  }
}

合成事件中setState的调用栈分析

​ 合成事件中的setState比较常见,onClick事件里去更新this.state.count的值,整个合成事件到state更新我们可以通过断点看到整个调用栈,大概流程如下:

state_process.png

​ 从上面可以看到从我们点击到执行原生的click事件之间react做了大量的合成事件处理逻辑,到callCallback这里我们代码只是走了合成事件的处理流程,从setState到requestWork是调用this.setState后的相关逻辑。

​ 我们这里看一下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);
  }
}

​ 在 requestWork 中有三个if分支,三个分支中有两个方法 performWorkOnRootperformSyncWork ,就是我们默认的update函数,但是在合成事件中,走的是第二个if分支,第二个分支中有两个标识 isBatchingUpdatesisUnbatchingUpdates 两个初始值都为 false ,但是在 interactiveUpdates$1 中会把 isBatchingUpdates 设为 true ,下面就是 interactiveUpdates$1 的代码:

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();
    }
  }
}

​ 在这里isBatchingUpdates状态被置为true, 而isUnbatchingUpdatesfalse, 所以在requestWork方法中直接被return了,return之后回到interactiveUpdates$1方法中,上面说到,从dispatchEventrequestWork整个都是在interactiveUpdates$1方法的try代码块中,也就是这个fn(a, b),所以在你在合成事件中setState后立即去console.log时,此时state并没有完成更新,所以console.log是无法拿到最新的state的值。这就导致了state更新的“异步”。

​ 但是当你的try代码块执行完的时候(也就是你的increment合成事件),这个时候会去执行 finally 里的代码,在 finally 中执行了 performSyncWork 方法,这个时候才会去更新你的 state 并且渲染到UI上。

生命周期钩子中的setState

class App extends React.Component {
  state = { count: 0 };
  
  componentDidMount(){
      this.setState({ count: this.state.count + 1 });
      console.log(this.state.count);
  }

  render() {
    return <button onClick={this.onClick} />;
  }
}

​ 其实生命周期钩子中的setState和合成事件一样,当 componentDidmount 执行的时候,react内部并没有更新,执行完componentDidmount 后才去 commitUpdateQueue 更新。这就导致你在 componentDidmountsetState 后,console.log输出的结果还是更新前的值。

原生事件中的setState

class App extends React.Component {
  state = { count: 0 };
  
  addCount = () => {
      this.setState({ count: this.state.count + 1 })
  }
  
  componentDidMount(){
      document.body.addEventListener('click', this.addCount, false);
  }

  render() {
    return <div>{this.state.count}</div>;
  }
}

原生事件是指JavaScript自带的事件监听,通过addEventListenerdocument.querySelector().onclick这种习形式绑定的事件,与react的合成事件相区别。

在原生事件触发后并不会走合成事件那一大堆逻辑,直接触发handle方法后执行setState,setState后代码执行到前面合成事件里说过的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);
  }
}

​ 在原生事件中,requestWork方法isRenderingisBatchingUpdates状态都为false, 所以这里不会像合成事件触发时在这里被return,而是会直接走进expirationTime === Sync分支,执行performSyncWork方法去处理更新State。所以在原生事件中,你可以在setState后同步拿到最新的state值。

setTimeout中的setState

​ 那在setTimeout中执行的setState又是如何可以同步拿到最新的值的呢?setTimeout可以在合成事件里执行,也可以在原生事件、生命周期钩子里执行,它是如何保证能够同步获取最新state的呢?

​ 基于EventLoop模型,我们知道setTimeout属于宏任务。那么在合成事件中,try块代码里执行到setTimeout时会将它放入宏任务队列,而先执行finally代码块的更新部分逻辑,当finally代码块执行完毕后,isBatchingUpdates状态也会被置为false,这就导致最后在执行setTimeout部分setState逻辑时,在requestWork方法中会走跟原生事件相同的expirationTime === Sync分支,所以setTimeout里面setState后可以拿到最新的state了。

总结

​ 通过上面的分析我们可以知道,react合成事件和生命周期方法中,setState是“异步”的,在原生事件和setTimeout中,setState是同步的

setState的异步并不是说其内部由异步代码实现,其实本身逻辑代码都是同步的,只不过在合成事件及生命周期方法中获取state的部分代码在state更新之前被调用,导致没办法立刻拿到最新的state,从而形成所谓的“异步”。

​ 以上就是这篇文章的全部内容了,主要结合部分源码介绍了不同场景下setState的执行与更新逻辑。如果对你有帮助,不要忘了给我点赞转发收藏三连,如果有任何想法欢迎在下方留言。

​ 我是小柒,我们下期见。