Skip to content

Instantly share code, notes, and snippets.

@ajhsu
Last active May 14, 2018 14:12
Show Gist options
  • Save ajhsu/e259392f06aa8e3bf5c9 to your computer and use it in GitHub Desktop.
Save ajhsu/e259392f06aa8e3bf5c9 to your computer and use it in GitHub Desktop.
Dive into setState method in React

Dive into setState() method in React

起因

與伊神哥在討論其中一段程式碼時,發現某一段程式碼在 setTimeout() 裡面執行結果如預期,但搬出 setTimeout() 之後卻會有問題。

1. 以下僅為示意程式碼,並非案例當事人。
2. 其實 setTimeout() 在原本的案例是 Promise Callback,為了解釋則替換為 setTimeout()

// Example 1
// 原先執行結果如預期的程式碼
class Component extends React.Component {
  constructor() {
    this.data = [];
  }
  render() {
    return (
      <section>
        {JSON.stringify(this.state.data)}
      </section>
    );
  }
  componentDidMount() {
    setTimeout(() => {
      this.setState({
        data: [...this.state.data, 1]
      });
      this.setState({
        data: [...this.state.data, 2]
      });
      this.setState({
        data: [...this.state.data, 3]
      });
    }, 0);
  }
}
// Latest Output:
// [1, 2, 3]

不過伊神哥因為某些原因、某些理由,決定把那三組 this.setState()setTimeout() 裡面搬出來,導致了輸出結果不正常,如下所示:

// Example 2
// 執行結果不如預期的程式碼
class Component extends React.Component {
  constructor() {
    this.data = [];
  }
  render() {
    return (
      <section>
        {JSON.stringify(this.state.data)}
      </section>
    );
  }
  componentDidMount() {
    this.setState({
      data: [...this.state.data, 1]
    });
    this.setState({
      data: [...this.state.data, 2]
    });
    this.setState({
      data: [...this.state.data, 3]
    });
  }
}
// Latest Output:
// [3]

很明顯可以發現,最後的結果僅留下了第三次執行 setState() 的結果。
於是開始思考,為什麼會這樣呢?為什麼呢為什麼呢為什麼呢?

研究方向

分為兩件事來探討

  1. setState() 的行為
  2. setTimeout() 的作用

setState 的同步/非同步行為

引用 官方文件 的解釋:

setState() does not immediately mutate this.state but creates a pending state transition. Accessing this.state after calling this method can potentially return the existing value. There is no guarantee of synchronous operation of calls to setState and calls may be batched for performance gains.

也就是官方不保證 setState() 的行為一定是同步執行的,因此希望開發者一律將他視為非同步執行 (以結論來說,我們也應該這麼做)。

事實是,setState 會依照執行情境的不同,有可能會是同步或非同步

非同步情境

  • 發生情境:當 setState() 被呼叫的位置,在 React Component 能掌管的範圍內。
  • setState() 的位置在 React Component 的 Life Cycle 或是其延伸的 Event Handlers 當中。
  • 也就是官方預期的執行情境
  • Demo

同步情境

  • 發生情境:當 setState() 被呼叫的位置,不在 React Component 能掌管的範圍內。
  • setState() 已經脫離了 React Component 的 Life Cycle 或是其延伸的 Event Handlers
  • 例如:在 setTimeout()addEventListener() 的 Callback 裡面呼叫 setState()
  • Demo

原因

為什麼會有上述兩種差異,主要的原因就是 React 希望降低 Re-render 的次數,因此在 React 能夠掌控的範圍內 (如 onClick 等 SyntheticEvent),會打包事件裡所有的操作之後再一併進行更新。
這在 React 的運作機制裡叫做 Update Batching
#Ref1#Ref2

延伸閱讀


setTimeout(Callback, 0)

  • Q、有時候我們會看到某些 JavaScript Developer 會使用 setTimeout(Callback, 0) 來呼叫一組函式,那背後的原因是什麼呢?
  • A、用兩個方向來解釋
    • 以目的來說:跳脫現有的 Synchronous Call,讓 Callback 成為一個 Asynchronous Call
    • 以原理來說:因為 JavaScript 是使用單執行緒逐行地執行指令,使用 setTimeout(Callback, 0) (不管 delay 是否為 0) 會讓這個 Callback 被排在此次 Synchronous Call 執行完畢之後,Callback 才會被執行到。間接達成 Callback 成為 Asynchronous Call 的目的。
    • 而事實上 setTimeout 設定為 0 毫秒,並不會真的就在 0 毫秒之後執行。#Ref

參考資料


結論

總結來說,在一開始的兩個範例當中:

  1. 範例二的 3 個 setState()componentDidMount() 之中,所以對 setState() 來說,會因為 Update Batching 機制,在函式的最後被打包在一起執行,也就是當 setState() 真正被執行的期間,this.state 是完全沒有被更新的狀態(因為 setState() 原本就是一個 Asynchronous Call),也就是為什麼最後的 State 會只有第三次的執行結果。

  2. 範例一的 3 個 setState()setTimeout() 之中,已經成為了 Asynchronous Call,所以對 setState() 來說,就脫離了 React Component 掌控的範圍,因此 setState() 會變成 Synchronous Call,所以三次的 Execution 都可以順利抓到上一個 Execution 更新的結果。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment