logo
Published on

為何說setState方法是異步的

Authors
  • avatar
    Name
    Eddy Chang
    Twitter

註: 2019-3-9 本文撰寫於 React Fiber 改進之前,有點時間了,不過內容仍然可以參考

在學習或使用過一陣子 React 後,你可能會發現一個在setState方法的特性,以下面這個簡單範例來說明:

export default class SelectBox extends React.Component {
  constructor(props) {
    super(props)
    this.state = { value: '' }
  }

  handleChange = e => {
    this.setState({ value: e.target.value })
    console.log(this.state.value)
  }

  render() {
    return (
      <div>
        <select onChange={this.handleChange} value={this.state.value}>
          <option value="JavaScript" key={1}>
            JavaScript
          </option>
          <option value="Angular2" key={2}>
            Angular2
          </option>
          <option value="React" key={3}>
            React
          </option>
        </select>
        <h1>{this.state.value}</h1>
      </div>
    )
  }
}

我們在handleChange方法中,呼叫setState來更新選項的值,然後在控制台中輸出這個值。看起來一切都是很符合邏輯,但你如果一執行就會發現,在控制台中輸出的this.state.value,並不會在呼叫setState方法後立即就變動。像下面的執行的結果圖一樣:

執行結果

當然,如果你直接輸出的是e.target.value,一定是正確的值,但在某些情況下,我們要取用的並不是這個事件的值,而是要更動過後的 state(狀態)值。

如果要在setState方法後,直接取用更動後的state值,正確的使用方式,在官方文件中的說明,需要利用setState的第二參數值,傳入一個 callback 函式,改為像下面這樣的程式碼:

this.setState({ value: e.target.value }, function() {
  console.log(this.state.value)
})

另一個方式則是用componentDidUpdate()這個生命週期方法,把確定 state 更新後的程式碼放在裡面,如下面的程式碼:

componentDidUpdate(){
  console.log(this.state.value)
}

為什麼一定要這樣作的主要原因是:

setState這個方法,它在 React 中的執行行為可以認為"異步的"。

雖然setState並非使用像使用了setTimeout或 promise 的那種進入到事件迴圈(Event loop)的異步執行,但它的執行行為在 React 函式庫中時,的確是異步的,也就是有延時執行的行為。但以官方文件中較為精確的說法,"它不是保證同步的"。

setState方法與包含在其中的執行是一個很複雜的過程,這段程式碼從 React 最初的版本到現在,也有無數次的修改。它的工作除了要更動this.state之外,還要負責觸發重新渲染(render),這裡面要經過 React 核心中 diff 演算法,最終才能決定是否要進行重渲染,以及如何渲染。而且為了批次與效能的理由,多個setState呼叫有可能在執行過程中還需要被合併,所以它被設計以異步的或延時的來進行執行是相當合理的。

那麼setState會在何時以同步的方式來執行,也就是立即更動this.state?答案是在 React 函式庫控制之外時,它就會以同步的方式來執行,在下面兩篇文章中,都有類似的例子:

但大部份的使用情況下,我們都是使用了 React 函式庫中的表單元件,例如 select、input、button 等等,它們都是 React 庫中人造的元件與事件,是處於 React 函式庫的控制之下,在這個情況下,setState就會以異步的方式執行。所以一般來說,我們會認為setState就是異步執行,並不是用原始碼來看說它是不是有使用像setTimeoutPromise之類的方式轉為 JavaScript 的異步執行方式,而是以它在 React 庫的控制之下,以執行行為與順序來認定。

以下是翻譯自官方 setState 原始碼的註解,官網的說明也是類似的說明:

不保證this.state會立即更新,所以在調用這個方法後存取this.state可能會回傳舊的值。

不保證呼叫setState就會同步地執行,而它們也可能最終被被批量呼叫(多次呼叫的情況下)。你可以提供額外的回調(callback),回調(callback)將會在setState實際被完成時被執行。

因此,很早就有開發者提出來關於setState常令初學者感到怪異的執行情況,在某些情況下會造成執行上會看到不連續的結果。除了setState方法有異步執行的行為外,它還有幾個被提出來特殊行為:

1. setState 可能會引發不必要的渲染(renders)

setState的設計是用來更動state值,也會觸發重新渲染(re-render),按照邏輯就是反正不管如何,只要開發者呼叫setState,React 就去作整個視圖的重新渲染就是。所以setState必定會作重新渲染的執行,只是要如何渲染是由 React 來決定。

重新渲染(re-render)指的主要是網頁上視圖(View)的重新再呈現,這是 React 原本的核心設計,這個設計是有一些問題的。最主要的是 state(狀態)並不一定單純只用來記錄與視圖(View)有關的狀態,也有可能是某個內部控制用的屬性值,或是只應用內部使用的資料。當你改變了這些與視圖無關的 state(狀態)值,以現在的 React 設計來說,照樣要觸發重新渲染的執行過程,這在某些複雜的應用時,由於造成不必要的渲染,有可能造成效能上的問題。

當然,React 提供了shouldComponentUpdate方法讓開發者可以自行判斷,自行提供對應的解決方式。也有Performance Tools可以進行剖析檢測。算得上是一些補強的作法。

2. setState 無法完全掌控應用中所有元件的狀態

state(狀態)是獨立於每個元件內部的,而且它是個不能直接更動的物件值,這個設計當然是為了要保持元件的封裝與獨立性,但所以如果當要開發一個複雜的應用時,必定需要使用那些能掌控所有元件資料,以及能提供各元件間資料互動的函式庫,例如 Flux, Redux 或 MobX 等等。

React 元件目前只能透過各種生命週期的方法,與外部資源、計時器或 DOM 事件來進行掛勾(Hook),這些都無法直接使用setState方法來進行,因此setState並沒有辦法完全掌控一個應用中所有元件的狀態,它像是每個元件中的都有的一種接口方法,單純要依靠setState方法來管控整個 React 應用,完全是不足夠的。

以上說明參考自這篇文章: 3 Reasons why I stopped using React.setState