React setState是同步仍是異步的

環境

React 16.9.0 React-Dom 16.9.0bash

前言

從下面代碼的運行結果能夠得出以下結論:app

  • 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)。來標記其如今所處的執行環境。post

executionContext默認值
合成事件

生命週期中的處理

而在setTimeout以及原生事件中,是脫離了這些執行環境的,executionContext就是默認值NoContext;。下圖爲原生事件執行時的截圖ui

原生事件執行時

scheduleWork處理邏輯的時候,若是執行環境不爲NoContext,則僅僅是將更新放在一個隊列裏面,不進行實際的應用(即調用flushSyncCallbackQueue)。this

結論

是不是同步更新的,取決於其執行環境。由於setTimeout和原生事件脫離了本來的執行環境,因此其state的更新爲同步更新。spa

「異步」場景下,何時對state進行的更新

那在合成事件和生命週期中,又是何時調用的flushSyncCallbackQueue,下面代碼中的輸出又是什麼?3d

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在處理一次點擊事件時,將全部的回調放在了一個隊列裏面。code

參考 juejin.im/post/5d65e5…component

就是在該隊列執行完畢以後調用的flushSyncCallbackQueue。所以上面的示例代碼中,全部的打印都爲0;

其執行過程能夠用以下代碼簡單表示:

var a = 1;
var updateQueue = [];

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

function func () {
  updateQueue = [];
    try {
        // 將本次須要調用的放在一塊兒
       setState({a: 1});
       console.log(window.a);
       setState({a: 3});
    } finally {
        // 模擬最後一次性提交更新
        window.a = updateQueue.reduce((accumulator, currentValue) => {
          return currentValue.a || accumulator;
        }, window.a)
    }
}

// 運行
func()
console.log(window.a);
複製代碼

結論

對生命週期或者合成事件包裹了一層try { // 執行,更新放隊列 } finally { // 更新state },最後在finally中進行的state更新

相關文章
相關標籤/搜索