---
# System prepended metadata

title: '[React] 面試題目：React 中`setState` 是同步執行嗎？還是非同步執行的？'
tags: [React, 面試題目, 前端筆記]

---

# [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` 是非同步執行...）

![](https://hackmd.io/_uploads/BkYF7fJI2.png)

```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`
![](https://hackmd.io/_uploads/HJfaxr1I3.png)

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

![](https://hackmd.io/_uploads/SJckZB1Un.png)

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

![](https://hackmd.io/_uploads/rkmLZSy83.png)

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 的結果是什麼

![](https://hackmd.io/_uploads/rJ7mrB18n.png)

## 參考資料

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)