# [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)