參考文章
把一系列 state 更新加入隊列
設置組件 state 會把一次重新渲染加入隊列。但有時可能會希望在下次渲染加入隊列之前對 state 的值執行多次操作。為此,了解 React 如何批量更新 state 會很有幫助。
React 會對 state 更新進行批處理
在下面的示例中,可能會認為點擊 “+3” 按鈕會使計數器遞增三次,因為它調用了 setNumber(number + 1)
三次:
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 1);setNumber(number + 1);setNumber(number + 1);}}>+3</button></>)
}
但是,每一次渲染的 state 值都是固定的,因此無論調用多少次 setNumber(1)
,在第一次渲染的事件處理函數內部的 number
值總是 0
:
setNumber(0 + 1);
setNumber(0 + 1);
setNumber(0 + 1);
但是這里還有另外一個影響因素需要討論。React 會等到事件處理函數中的 所有 代碼都運行完畢再處理 state 更新。 這就是為什么重新渲染只會發生在所有這些 setNumber()
調用 之后 的原因。
這讓你可以更新多個 state 變量——甚至來自多個組件的 state 變量——而不會觸發太多的 重新渲染。但這也意味著只有在事件處理函數及其中任何代碼執行完成 之后,UI 才會更新。這種特性也就是 批處理,它會使 React 應用運行得更快。它還會幫你避免處理只更新了一部分 state 變量的令人困惑的“半成品”渲染。
React 不會跨 多個 需要刻意觸發的事件(如點擊)進行批處理——每次點擊都是單獨處理的。請放心,React 只會在一般來說安全的情況下才進行批處理。這可以確保,例如,如果第一次點擊按鈕會禁用表單,那么第二次點擊就不會再次提交它。
在下次渲染前多次更新同一個 state
這是一個不常見的用例,但是如果想在下次渲染之前多次更新同一個 state,可以像 setNumber(n => n + 1)
這樣傳入一個根據隊列中的前一個 state 計算下一個 state 的 函數,而不是像 setNumber(number + 1)
這樣傳入 下一個 state 值。這是一種告訴 React “用 state 值做某事”而不是僅僅替換它的方法。
現在嘗試遞增計數器:
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(n => n + 1);setNumber(n => n + 1);setNumber(n => n + 1);}}>+3</button></>)
}
在這里,n => n + 1
被稱為 更新函數。當將它傳遞給一個 state 設置函數時:
- React 會將此函數加入隊列,以便在事件處理函數中的所有其他代碼運行后進行處理。
- 在下一次渲染期間,React 會遍歷隊列并給你更新之后的最終 state。
setNumber(n => n + 1);
setNumber(n => n + 1);
setNumber(n => n + 1);
下面是 React 在執行事件處理函數時處理這幾行代碼的過程:
setNumber(n => n + 1)
:n => n + 1
是一個函數。React 將它加入隊列。setNumber(n => n + 1)
:n => n + 1
是一個函數。React 將它加入隊列。setNumber(n => n + 1)
:n => n + 1
是一個函數。React 將它加入隊列。
當在下次渲染期間調用 useState
時,React 會遍歷隊列。之前的 number
state 的值是 0
,所以這就是 React 作為參數 n
傳遞給第一個更新函數的值。然后 React 會獲取上一個更新函數的返回值,并將其作為 n
傳遞給下一個更新函數,以此類推:
更新隊列 | n | 返回值 |
---|---|---|
n => n + 1 | 0 | 0 + 1 = 1 |
n => n + 1 | 1 | 1 + 1 = 2 |
n => n + 1 | 2 | 2 + 1 = 3 |
React 會保存 3
為最終結果并從 useState
中返回。
這就是為什么在上面的示例中點擊“+3”正確地將值增加“+3”
如果在替換 state 后更新 state 會發生什么
這個事件處理函數會怎么樣?你認為 number
在下一次渲染中的值是什么?
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);}}>增加數字</button></>)
}
這是事件處理函數告訴 React 要做的事情:
setNumber(number + 5)
:number
為0
,所以setNumber(0 + 5)
。React 將 “替換為5
” 添加到其隊列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函數。 React 將 該函數 添加到其隊列中。
在下一次渲染期間,React 會遍歷 state 隊列:
更新隊列 | n | 返回值 |
---|---|---|
“替換為 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
React 會保存 6
為最終結果并從 useState
中返回。
注意:setState(x)
實際上會像 setState(n => x)
一樣運行,只是沒有使用 n
!
如果在更新 state 后替換 state 會發生什么
讓我們再看一個例子。你認為 number
在下一次渲染中的值是什么?
import { useState } from 'react';export default function Counter() {const [number, setNumber] = useState(0);return (<><h1>{number}</h1><button onClick={() => {setNumber(number + 5);setNumber(n => n + 1);setNumber(42);}}>增加數字</button></>)
}
以下是 React 在執行事件處理函數時處理這幾行代碼的過程:
setNumber(number + 5)
:number
為0
,所以setNumber(0 + 5)
。React 將 “替換為5
” 添加到其隊列中。setNumber(n => n + 1)
:n => n + 1
是一個更新函數。React 將該函數添加到其隊列中。setNumber(42)
:React 將 “替換為42
” 添加到其隊列中。
在下一次渲染期間,React 會遍歷 state 隊列:
更新隊列 | n | 返回值 |
---|---|---|
“替換為 5 ” | 0 (未使用) | 5 |
n => n + 1 | 5 | 5 + 1 = 6 |
“替換為 42 ” | 6 (未使用) | 42 |
然后 React 會保存 42
為最終結果并從 useState
中返回。
總而言之,以下是可以考慮傳遞給 setNumber
state 設置函數的內容:
- 一個更新函數(例如:
n => n + 1
)會被添加到隊列中。 - 任何其他的值(例如:數字
5
)會導致“替換為5
”被添加到隊列中,已經在隊列中的內容會被忽略。
事件處理函數執行完成后,React 將觸發重新渲染。在重新渲染期間,React 將處理隊列。更新函數會在渲染期間執行,因此 更新函數必須是 純函數 并且只 返回 結果。不要嘗試從它們內部設置 state 或者執行其他副作用。在嚴格模式下,React 會執行每個更新函數兩次(但是丟棄第二個結果)以便幫助你發現錯誤。
命名慣例
通常可以通過相應 state 變量的第一個字母來命名更新函數的參數:
setEnabled(e => !e);
setLastName(ln => ln.reverse());
setFriendCount(fc => fc * 2);
如果你喜歡更冗長的代碼,另一個常見的慣例是重復使用完整的 state 變量名稱,如 setEnabled(enabled => !enabled)
,或使用前綴,如 setEnabled(prevEnabled => !prevEnabled)
。
摘要
- 設置 state 不會更改現有渲染中的變量,但會請求一次新的渲染。
- React 會在事件處理函數執行完成之后處理 state 更新。這被稱為批處理。
- 要在一個事件中多次更新某些 state,可以使用
setNumber(n => n + 1)
更新函數。