- Published on
為何說setState方法是異步的
- Authors
- Name
- Eddy Chang
註: 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 函式庫控制之外時,它就會以同步的方式來執行,在下面兩篇文章中,都有類似的例子:
- ON THE ASYNC NATURE OF
SETSTATE
IN REACT - setState() State Mutation Operation May Be Synchronous In ReactJS
但大部份的使用情況下,我們都是使用了 React 函式庫中的表單元件,例如 select、input、button 等等,它們都是 React 庫中人造的元件與事件,是處於 React 函式庫的控制之下,在這個情況下,setState
就會以異步的方式執行。所以一般來說,我們會認為setState
就是異步執行,並不是用原始碼來看說它是不是有使用像setTimeout
或Promise
之類的方式轉為 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