React-setState雜記

菜的黑人牙膏發表於2019-02-12

前言

在看React的官方文件的時候, 發現了這麼一句話,State Updates May Be Asynchronous,於是查詢了一波資料, 最後歸納成以下3個問題

  • setState為什麼要非同步更新,它是怎麼做的?
  • setState什麼時候會非同步更新, 什麼時候會同步更新?
  • 既然setState需要非同步更新, 為什麼不讓使用者可以同步讀到state的新值,但更新仍然是非同步?

常見場景下的非同步更新

以下是官方文件的一個例子, 呼叫了3次incrementCount方法, 期望this.state.count的值是3, 但最後卻是1

incrementCount() {
  this.setState({count: this.state.count + 1});
}

handleSomething() {
  // Let's say `this.state.count` starts at 0.
  this.incrementCount();
  this.incrementCount();
  this.incrementCount();
  // When React re-renders the component, `this.state.count` will be 1, but you expected 3.

  // This is because `incrementCount()` function above reads from `this.state.count`,
  // but React doesn't update `this.state.count` until the component is re-rendered.
  // So `incrementCount()` ends up reading `this.state.count` as 0 every time, and sets it to 1.

  // The fix is described below!
}
複製程式碼

那麼就可以引出第一個問題

setState為什麼要非同步更新,它是怎麼做的?

深入原始碼你會發現:(引用程墨老師的setState何時同步更新狀態

在 React 的 setState 函式實現中,會根據一個變數 isBatchingUpdates 判斷是直接更新 this.state 還是放到佇列中回頭再說, 而 isBatchingUpdates 預設是 false,也就表示 setState 會同步更新 this.state, 但是,有一個函式 batchedUpdates,這個函式會把 isBatchingUpdates 修改為 true, 而當 React 在呼叫事件處理函式之前就會呼叫這個 batchedUpdates,造成的後果,就是由 React 控制的事件處理過程 setState 不會同步更新 this.state。

然後我在網上引用了這張圖(侵刪)

image

從結論和圖都可以得出, setState是一個batching的過程, React官方認為, setState會導致re-rederning, 而re-rederning的代價是昂貴的, 所以他們會盡可能的把多次操作合併成一次提交。以下這段話是Dan在Issue中的回答:

image

中心意思大概就是:
同步更新setState並re-rendering的話在大部分情況下是無益的, 採用batching會有利於效能的提升, 例如當我們在瀏覽器插入一個點選事件時,父子元件都呼叫了setState,在batching的情況下, 我們就不需要re-render兩次孩子元件,並且在退出事件之前re-render一次即可。

那麼如果我們想立即讀取state的值, 其實還有一個方法, 如下程式碼:
因為當傳入的是一個函式時,state讀取的是pending佇列中state的值

incrementCount() {
  this.setState((state) => {
    // Important: read `state` instead of `this.state` when updating.
    return {count: state.count + 1}
  });
}

handleSomething() {
  // Let's say `this.state.count` starts at 0.
  this.incrementCount();
  this.incrementCount();
  this.incrementCount();

  // If you read `this.state.count` now, it would still be 0.
  // But when React re-renders the component, it will be 3.
}
複製程式碼

當然, 仔細看React文件的話, 可以發現, State Updates May Be Asynchronou裡面有一個may的字眼,也就是可能是非同步更新, 因而引出第二個問題

setState什麼時候會非同步更新, 什麼時候會同步更新?

其實從第一個問題中我們就知道,React是根據isBatchingUpdates來合併更新的, 那麼當呼叫setState的方法或者函式不是由React控制的話, setState自然就是同步更新了。

簡單的舉下例子:

  1. 如componentDidMount等生命週期以及React的事件即為非同步更新,這裡不顯示具體程式碼。
  2. 如自定義的瀏覽器事件,setTimeout,setInterval等脫離React控制的方法, 即為同步更新, 如下(引用程墨老師的setState何時同步更新狀態
componentDidMount() {
  document.querySelector('#btn-raw').addEventListener('click', this.onClick);
}
onClick() {
  this.setState({count: this.state.count + 1});
  console.log('# this.state', this.state);
}
// ......
render() {
  console.log('#enter render');
  return (
    <div>
      <div>{this.state.count}
        <button id="btn-raw">Increment Raw</button>
      </div>
    </div>
  )
}
複製程式碼

有的人也會想能不能React依然合併更新, 但使用者可以同步讀取this.state的值, 這個問題在React的一個Issue上有提到, 也是我們的第三個問題

既然setState需要非同步更新, 為什麼不讓使用者可以同步讀到state的新值,但更新仍然是非同步?

這個問題可以直接在Dan的回答中得到:

This is because, in the model you proposed, this.state would be flushed immediately but this.props wouldn’t. And we can’t immediately flush this.props without re-rendering the parent, which means we would have to give up on batching (which, depending on the case, can degrade the performance very significantly).

大概意思就是說:

如果在應用中,this.state的值是同步,但是this.props卻不是同步的。因為props只有當re-rendering父元件後才傳給子元件,那麼如果要props變成同步的, 就需要放棄batching。 但是batching不能放棄。