# [React] 面試題目:React 中`setState` 是同步執行嗎?還是非同步執行的?
###### tags: `React` `前端筆記` `面試題目`
## 題目
本週面試時有被問下列程式碼,並且詢問在 React 18+ 及 React 18+ 以前 `console.log(a)` 的順序是什麼?以及 `setState` 是非同步執行還是同步執行:
```javascript!
export default function App() {
const [a, setA] = useState(0);
useEffect(() => {
setTimeout(() => {
setA(4);
console.log('middle');
setA(5);
});
}, []);
console.log(a);
return (
<div className="App">
<p>{a}</p>
</div>
);
}
```
## 面試時的思路
當時思考的思路:
1. 每一次 render 所有東西(`state, props, fucntions`)都是自己的 scope
2. ==叫用 `setState` 時不會馬上執行比較及重新叫用元件==
3. 為了優化效能,如該次 handler 中有叫用數次 `setState`,那麼 React 會把 `setState` 排程,所以會盡可能地減少元件重新叫用的次數(這個行為被稱作 batch update,batch n. 一批)
```javascript!
// 所以即便這樣子寫,兩個 useEffect() 中 setStateFunctions 也會被排程一次叫用元件
export default function App() {
const [a, setA] = useState(0);
const [b, setB] = useState("b");
// 因為 18+ 之後 React 就會自動 batching update,所以元件只會 render 兩次
// 初始化 -> effect function 內的 setStateFunctions 被擠成一個 queue
useEffect(() => {
setTimeout(() => {
setA(4);
setA(5);
}, 0);
}, []);
useEffect(() => {
setA(100);
setB("c");
}, []);
console.log(a);
console.log(b);
return (
<div className="App">
<p>{a}</p>
</div>
);
}
// 0
// 'b'
// ------
// 100
// 'c'
// -----
// 5
// 'c'
// 可以把兩個 useEffect 想成這樣子(因為 effect functions 在第一叫用後一定會叫用)
const renderFn () => {
setA(100)
setB("C")
}
```
因為第二點的緣故,讓我以為 `setState` 是像 `setTimeout, Promise` 是需要透過 `event loop` 的概念持續觀察 `call stack` 是否為空,等到空了才會執行 `setState`:
```javascript!
// call stack
EC1
EC2
....
// event loop
// 會一直看 call stack 是否為空才會 render
setStateFunc queue...
```
但是我又沒辦法回答在 React 18+ 以前為什麼會在 `setTimeout()` 內會馬上觸發 `setState`,然後元件重新叫用完後才會執行下一個 `setState`(所以只能跳針重複闡述 `setState` 是非同步執行...)

```javascript!
// 0 -> 元件第一次叫用
// 4 -> 第一個 setState 叫用後,元件重新第二次叫用
// 'middle'
// 5 -> 第二個 setState 叫用後,元件重新第三次叫用
```
## 先回歸 React 更新機制
大概的執行流程:
`setState` 被叫用並傳入新的 `state` -> React 透過 `Object.is()` 比對前一次與新的 `state` 是否有不同 -> 無則停止動作 -> 有不同便執行 Reconciliation -> 重新叫用元件並建立 Virtual DOM Tree -> 將新的 Virtual DOM Tree 與前一次舊的 Virtual DOM Tree 比較 -> 只針對兩次 Virtual DOM Tree 的不同更新 real DOM(達到最小化更新 real DOM 的目標,藉此優化效能)
## batch update 不會像 `setTimeout, Promise` 等透過 `event loop` 非同步執行
但好險有大神分享的文章 [React 的 setState 是同步还是异步?](https://juejin.cn/post/7108362046369955847)(內有大神研究 React 原始碼的記錄,我也只是看大神整理好的資訊 QQ):
> 虽然我们讨论的是 setState 的同步异步,但这个不是 setTimeout、Promise 那种异步,只是指 setState 之后是否 state 马上变了,是否马上 render。
Dan 在 [Does React keep the order for state updates?](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973) 的回覆
> The key to understanding this is that no matter how many setState() calls in how many components you do inside a React event handler, they will produce only a single re-render at the end of the event.
>(最重要的概念是無論在一個 React event handler 叫用多少次 setState(),它們只會產生一次 render)
React 是必須透過重新叫用元件才可以讓元件讀取到最新的資訊,所以 batch update 只是避免元件重複叫用優化的手段:
```javascript!
setState(1) // 如果這邊就重新叫用一次
setState(2)
// 那麼元件就會叫用兩次才可以得到 2...
```
所以 `setState` 其實本身是同步執行的,並不是像是 `setTimeout, Promise` 等的非同步執行。但因為 React 透過 batch update 優化元件叫用(也就是 render)的次數以及叫用 `setState` 的時機點,所以才會使得 `setState` 像是非同步執行的。
## 為什麼 React 18+ 以前在非同步的情況下會沒辦法 batch update?
> [...] we are inside a React event handler where batching is enabled (because React "knows" when we're exiting that event).
> 在 React event handler 中 batch update 會被啟用(因為 React 知道什麼時候會結束 event,所以知道什麼時候才需要執行 batch update)。
> Internally React event handlers are all being wrapped in unstable_batchedUpdates which is why they're batched by default.
> React 自身的 handlers 在底層都有被 `unstable_batchedUpdates` 包覆,所以才可以在 handlers 內叫用多個 `setState` 時,state 才會遵從 batch update。
所以可以想像成這樣:
```javascript!
const handler = () => {
// 有 unstable_batchedUpdates 的幫助下才會 batch update
ReactDOM.unstable_batchedUpdates(() => {
setA('1')
setB('2')
setC('3')
setD('4')
setE('5')
setF('6')
})
}
```
但是非同步的 `setTimeout, Promise` 等並不是 React 自身的事件,因此並不會被 `unstable_batchedUpdates` 包覆,自然就不會有 batch update 的功能。
所以在下方的程式碼中,因為 `setState` 是同步執行的,所以自然就是「後進先出」,就會先觸發 `setState`,並又因為 `state` 確實有所不同而導致元件重新叫用:
```javascript!
// 省略不重要的程式碼 ...
useEffect(() => {
setTimeout(() => {
setA(4); // 因為沒有 batch update,所以 setState 會直接叫用 -> 後續的比對及叫用元件...
setA(5); // 因為沒有 batch update,所以 setState 會直接叫用 -> 後續的比對及叫用元件...
console.log('hello') // 會等到前面兩次元件重新叫用完才會執行...
});
}, []);
```
以圖像化 call stack 就會像是這樣子:
Step 1: 先執行第一個 `setState`

Step 2: 叫用元件的 Execution Context 被推到 call stack 最上面先執行

Step 3: 元件結束渲染,因此該 Execution Context 被移除,繼續執行下一個 Execution Context

Step 4: 等到第二次渲染元件結束後,該 Execution Context 被移除,才會繼續執行下一個 Execution Context(也就是 effection function 接下來的程式碼)
## 那要怎麼讓 React 18+ 以前的版本也使用 batch update?
由上方可知,React 是透過 `unstable_batchedUpdates` API 達到 batch update,那開發者只要手動寫入這個 API 就可以了:
```javascript!
// ref. https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973
promise.then(() => {
// Forces batching
ReactDOM.unstable_batchedUpdates(() => {
this.setState({a: true}); // Doesn't re-render yet
this.setState({b: true}); // Doesn't re-render yet
this.props.setParentState(); // Doesn't re-render yet
});
// When we exit unstable_batchedUpdates, re-renders once
});
```
## 所以 batch update 是像 event loop 一樣非同步執行嗎?
不,batch update 只是 React 優化元件渲染的手段,React 會知道什麼時候該叫用(已經合併的)`setState`,盡可能以最少的 render 次數達到 `state` 更新的目標。
> 但要記得心法:不可能是遇到一個 `setState` 就要重新叫用元件,因為這樣子在叫用數個 `setState` 就會導致元件叫用數次,也會使==該 render 中有自己的 state, props, functions 這個觀念難以實踐==。
```javascript!
const handleClick = () => {
setA('A') // 如果每次遇到 setState 就重新叫用元件
console.log(a) // 那麼在這段 handler 中就難以得知 a 是屬於哪一次 render 的結果...
setA('B')
console.log(a)
setA('C')
console.log(a)
}
```
## 為什麼要一直強調是 React 18+ 以前的版本才會有這個情形?
> Starting from React 18, React batches all updates by default.
因為在 React 18+ 後,React 就會預設全部 batch update。
## Recap
- `setState` 本身是同步執行,是因為 batch update 才會看起來像是非同步執行(React 會知道哪時候才需要執行 `setState`)
- 所以在探討 `setState` 是同步/非同步,是在探討是否是立即 `setState` 還是走 batch update
- React 18+ 以後的版本就全部支援 batch update
- React 18+ 以前在非 React 自身的 handlers 時(如 `setTimeout, Promise`)等,想要有 batch update 的話需要手動加入 `unstable_batchedUpdates`(要不然就會直接觸發 `setState` 並依照 `state` 比對結果叫用元件)
- 其他自身的 hanlders 則是在底層有使用 `unstable_batchedUpdates`,所以才會 batch update
- 即使走 batch update,也不是非同步執行,只是 React 透過手段延遲 `setState` 執行,實際上還是在 call stack 內(因為 JavaScript 是單執行緒的語言)
- `setState` 的順序很重要,因為會影響 batch update 的結果是什麼

## 參考資料
1. [React 的 setState 是同步还是异步?](https://juejin.cn/post/7108362046369955847)(大神研究原始碼的記錄)
2. [UseState: Asynchronous or what?](https://www.youtube.com/watch?v=RAJD4KpX8LA&t=869s)
3. [Does React keep the order for state updates?](https://stackoverflow.com/questions/48563650/does-react-keep-the-order-for-state-updates/48610973#48610973)
4. [理解React的setState到底是同步還是非同步(下)](https://ithelp.ithome.com.tw/articles/10257994)