React 中 setState 是同步还是异步

在事件中

这一点可能大家都已经知道了,在 React 的合成事件中,更新会被批量处理。

 handleClick() {
    console.log('初始的时候,a 是 ' + this.state.a);
    this.setState({a: 2});
    console.log(this.state.a);
    this.setState({a: 3});
    console.log(this.state.a);
}
复制代码

image.png

在 React 的合成事件中,这些回调函数都被包裹了一个叫做 batchedUpdates 的函数

不同环境下,这个函数也会不同,在 ReactDOM 中,它在这里被设置。真实的实现是来自于 react-reconciler 这个包。

下面是 batchedUpdates 的实现:

export function batchedUpdates<A, R>(fn: A => R, a: A): R {
  const prevExecutionContext = executionContext;
  
  // 把当前执行上下文设置为 BatchedContext,之后在调度的时候就会提前退出
  executionContext |= BatchedContext;
  try {
    // 可以简单的把 fn 理解为我们的回调函数
    // 就比如上面示例中的 `handleClick`
    return fn(a);
  } finally {
    // 设置会之前的上下文
    executionContext = prevExecutionContext;
      
    // 结束后再次发起更新,就能达到批量更新的效果了
    flushSyncCallbacksOnlyInLegacyMode();
  }
}
复制代码
  1. batchUpdates 的具体实现

理解了上面的过程,相信你也就知道了为什么在目前的 React 版本中,事件回调函数里写的异步函数不会被批量处理,因为那些异步函数会在上面的 try...finally 语句之后才执行,此时已经脱离了批量更新的上下文了,也就不能起到批量更新的效果。

 handleClick() {
    console.log('初始的时候,a 是 ' + this.state.a);
    setTimeout(() => {
      this.setState({ a: 'settimout' })
      console.log(this.state.a);
    })
    new Promise((res) => res()).then(res => {
      this.setState({ a: 'promise' })
      console.log(this.state.a);
    })
  }

复制代码

下面这个结果显示了,数据都会被同步修改。

image.png

同时,我们也能知道,在 React 中绑定原生事件也不会有批量更新的效果,因为我们自己绑定的原生事件也没有被 batchedUpdates 函数包裹。如果你也想在原生事件里有批量更新的效果,可以手动加:

import { unstable_batchedUpdates } from 'react-dom';

unstable_batchedUpdates(() => {
    this.setState({
        a: 10
    })
     this.setState({
        a: 11
    })
})
复制代码

生命周期中

componentDidMount() {
    this.setState({
      a: 2
    })
    console.log(this.state.a)
    this.setState({
      a: 3
    })
    console.log(this.state.a);
}
复制代码

image.png

可见,在生命周期中是异步的,临近的 setState 会被合并成一个。为什么会这样呢?

React 的渲染大概分为 render 和 commit 两个阶段, render 阶段会把新的变更在 Fiber 树上描绘出来,等到 commit 阶段,会把这些变更真实的应用到 DOM 树上去,比如把新建的 DOM 节点挂载到页面上,删除 DOM 节点。

componentDidMount 的执行位置就是在 commit 阶段执行的。同时,在 commit 阶段,React 会标记当前的执行上下文为 CommitContext ,在这个上下文环境中,每当组件调用 setState 想发起更新,只会把更新加入到任务队列中,但是会在实际调度更新之前就退出。

等到当前所有需要提交到 DOM 的任务都执行完了,React 会再次发起调度,去执行剩余的更新任务。

  1. setState 的源码位置
  2. componentDidMount 调用位置
  3. commit 阶段完毕后,执行之前加入队列的任务

Concurrent Mode

假如开启了 Concurrent Mode,此时 setState 发起的调度都会走 react-sceduler,这个调度过程的基础就是使用异步函数实现的,所以,setState 不用分情况,都是异步的。当然了,也要排除直接调用 flushSync 的场景。

function handleClick(){
    flushSync(()=> {
            setCount((count) => count + 1);
    });

    flushSync(()=> {
            setCount(12);
    });
}
复制代码

おすすめ

転載: juejin.im/post/7035549705601810440