React setState是同步还是异步的

2,519 阅读3分钟

环境

React 16.9.0 React-Dom 16.9.0

前言

从下面代码的运行结果可以得出如下结论:

  • setTimeout和原生事件中,可以立即拿到更新结果。也就是同步
  • 在合成事件和生命周期中,不能立即拿到更新结果。也就是所谓的“异步”
  • 在合成事件和生命周期中,如果对同一个值进行多次setStatesetState的批量更新策略会对其进行覆盖,取最后一次的执行
class App extends React.Component {

  constructor () {
    super();
    this.state = {
      counter: 0
    };
  }

  componentDidMount() {
    // 生命周期中调用
    console.log("componentDidMount before: " + this.state.counter);
    this.setState({ counter: this.state.counter + 1 });
    // 此处不能实时更新
    console.log("componentDidMount after: " + this.state.counter);
    setTimeout(() => {
      // setTimeout中调用
      console.log("setTimeout before: " + this.state.counter);
      this.setState({ counter: this.state.counter + 1 });
      console.log("setTimeout after: " + this.state.counter);
    }, 0);
    document.getElementById("btn-2").addEventListener("click", this.btn2Click);
  }


  spanClick = () => {
    const { counter } = this.state;
    console.log("spanClick before: " + this.state.counter);
    this.setState({
      counter: counter + 1
    })
    this.setState({
      counter: counter + 2
    })
    // 此处不能实时更新
    console.log("spanClick after: " + this.state.counter);
  }

  btn2Click = () => {
    const { counter } = this.state;
    console.log("addEventListener btn2Click before: " + this.state.counter);
    this.setState({
      counter: counter + 1
    })
    // 此处可以实时更新
    console.log("addEventListener btn2Click after: " + this.state.counter);
  }

  render () {
    return (
    <div className="App">
      <span className="btn" onClick={(event) => this.spanClick(event)}>
        点击
      </span>
      <span className="btn-2" id="btn-2">
        点击2
      </span>
    </div>
    )};
}

// 打印结果。before与after相同即为“异步”,否则为同步
// componentDidMount before: 0
// componentDidMount after: 0
// setTimeout before: 1
// setTimeout after: 2
// spanClick before: 2
// spanClick after: 2
// addEventListener btn2Click before: 4
// addEventListener btn2Click after: 5

看过很多篇这样标题的文章,看到过很多描述不同但意思如上的结论,但是仍然有一些疑问:

  • 为什么setTimeout和原生事件会同步更新?
  • “异步”情况究竟是在什么时候对state进行的更新?

为什么setTimeout和原生事件会同步更新

究竟是同步更新还是异步更新,取决于代码的执行环境。React定义了一个内部变量executionContext(默认为NoContext),在进行合成事件和生命周期处理的时候,会首先给该变量赋值为DiscreteEventContext(合成事件)或executionContext &= ~BatchedContext; executionContext |= LegacyUnbatchedContext;(componentDidMount)。来标记其现在所处的执行环境。

executionContext默认值 合成事件

生命周期中的处理

而在setTimeout以及原生事件中,是脱离了这些执行环境的,executionContext就是默认值NoContext;。下图为原生事件执行时的截图

原生事件执行时

scheduleWork处理逻辑的时候,如果执行环境不为NoContext,则仅仅是将更新放在一个队列里面,不进行实际的应用(即调用flushSyncCallbackQueue)。

结论

是否是同步更新的,取决于其执行环境。因为setTimeout和原生事件脱离了原本的执行环境,所以其state的更新为同步更新。

“异步”场景下,什么时候对state进行的更新

那在合成事件和生命周期中,又是什么时候调用的flushSyncCallbackQueue,下面代码中的输出又是什么?

class App extends React.Component {

  constructor () {
    super();
    this.state = {
      counter: 0
    };
  }

  appClick = () => {
    console.log('--------appClick---------');
    const { counter } = this.state;
    console.log(this.state.counter);
    this.setState({
      counter: counter + 1
    })
    console.log(this.state.counter); // 会输出2还是0?
  }

  spanClick = () => {
    const { counter } = this.state;
    this.setState({
      counter: counter + 1
    })
    console.log(this.state.counter);
    this.setState({
      counter: counter + 2
    })
    console.log(this.state.counter);
  }

  render () {
    return (
    <div className="App" onClick={(event) => this.appClick(event)}>
      <span className="btn" onClick={(event) => this.spanClick(event)}>
        点击
      </span>
    </div>
    )};
}

如果对React原生事件有了解,以click事件为例,会知道React在处理一次点击事件时,将所有的回调放在了一个队列里面。

参考 juejin.cn/post/684490…

就是在该队列执行完毕之后调用的flushSyncCallbackQueue。因此上面的示例代码中,所有的打印都为0;

其执行过程可以用如下代码简单表示:

var a = 1;
var updateQueue = [];

function setState (payload) {
  updateQueue.push(payload);
}

function func () {
  updateQueue = [];
    try {
        // 将本次需要调用的放在一起
       setState({a: 1});
       // 输出1
       console.log(window.a);
       setState({a: 3});
       // 输出1
       console.log(window.a);
    } finally {
        // 模拟最后一次性提交更新
        window.a = updateQueue.reduce((accumulator, currentValue) => {
          return currentValue.a || accumulator;
        }, window.a)
    }
}

// 运行
func()
// 输出3
console.log(window.a);

结论

对生命周期或者合成事件包裹了一层try { // 执行,更新放队列 } finally { // 更新state },最后在finally中进行的state更新