Deep dive React re-render
React 從呼叫 setState 到畫面改變,中間發生了什麼事?
Deep dive React re-render系列文
1. React Reconciliation
2. Deep dive React re-render
就算寫 React 很多年,也不見得對 React 底層 re-render 機制完全了解 ,導致常會出現一些難以理解的 bug 以及 Bad performance,這一篇算是看了 Advanced React 這本書跟 一次打破 React 常見的學習門檻與觀念誤解系列 後打通任督二脈後的筆記,感謝前輩們的分享。
此篇為系列文第二篇,會先從 setState
到畫面改變中間發生什麼事破冰,再逐步探討什麼會導致 re-render、re-render 的對象、re-render 具體在 React 中的行為等等。強烈建議先看過第一篇 React Reconciliation,了解什麼是 Virtual DOM element、JSX、Virtual DOM Tree diffing 再來閱讀此篇可以更得心應手。
🔖 文章索引
1. React 從呼叫 setState 到畫面改變,中間發生了什麼事
2. When will the app re-render
3. re-render 對象
4. re-render 具體在 React 中的行為
5. 練習
React 從呼叫 setState 到畫面改變,中間發生了什麼事
re-render 流程
Re-render 發生在資料需要被 React 更新時,例如使用者在 input 上打字,那這時資料就要從 null 更新到 Hannah。
這也是 setState
的功用,拿來更新 state
,但 React 初學者對他認知大概只停留在這種初淺認識,並不清楚底層是如何運作的所以容易寫出難以預期的 bug。想要更厲害就必須了解 React 是怎麼跑的
const Parent = () => {
const [state, setState] = useState(null)
// some line use setState('Hannah') below...
return <Child/>
}
setState
方法觸發資料更新- 此時 React 會先以
Object.is()
方法來檢查新舊 state 是否不同,如果相同的話則判定資料沒有變化所以畫面不用更新,就會直接中斷接下來的流程。如果不同的話則代表資料有所變化,因此接續以下 re-render 流程 - 觸發 re-render 後,React 再次以新的 state 重新執行一次
Parent
這個 function,函式裡的所有東西都會被 re-create,不管理面的是函式、<Child/>
、useEffect
- React Build a tree of Virtual DOM of
<Child/>
element - 新舊 Virtual DOM Tree diffing 比較 (Reconciliation)
// Before //After
const Parent = { const Parent = {
[type: Child] [type: Child]
} }
5. 更新到瀏覽器: 將 diffing 的差異結果移交 react-dom
,並更新到真實的 DOM Tree 中,以完成瀏覽器畫面的更新
每一個流程裡都蘊含 React 底層運作的知識,接下來會一一探討。
When will the app re-render
整體而言只要遇到 state update 或 Context change 時就會 re-render ,列出以下四種狀況
state change
只要藉由 setState
讓 state
改變就會觸發 re-render (calling the component function)。
Parent re-render
當父層 component re-renders, 包在裡面的所有 children component 也會 re-render。
這邊要提的是 re-render 永遠都是往下更新,而不會往上更新
Context Change
當 Context Provider 改變,所有有用到 context 的 components 都會 re-render
Hooks changes
若在 hook 裡發生 state update 或 Context change,用這個 hook 的 components 就會 re-render
當只有 props changes 的時候是不會觸發 re-render 的 (除非使用了React.memo
、useMemo
)
// 以下就算按無數次 button 也不會觸發 re-render
const Parent = () => {
let isOpen = false
return <button onClick={() => {isOpen = true}}>Click</button>
}
re-render 對象
re-render 對象為 virtual DOM node
這邊的 re-render 對象是指 virtual DOM node (React elements) 而不是真實的 DOM Tree,畢竟 virtual DOM 也只是一些普通 object 而已,不像真實 DOM 直接與瀏覽器的渲染引擎綁定,需要浪費大量效能
re-render 具體在 React 中的行為
單純就是重新跑一次 component function 而已
re-render 具體在 React 中的行為就是
“以新的資料(props 或 state)重新再執行一次 component function,並產生新版的 React elements”
每一次 re-render 都會產生新的 Component function
每一次 render 都有自己獨立的 props、state 以及 event handlers
每一次 re-render 都會產生新的 Component function,每一個 function 裡面的 state
跟函式甚至 useEffect
都是獨立存在的,每次 render 出來 function 彼此都不相關,只是在 scope 內的命名相同而已。
function Component() {
const [state, setState] = useState([1, 2, 3]);
const [copyState, setCopy] = useState(state);
function handleClick() {
setState([4, 5, 6]);
setState(state);
}
return (
<>
<p>{state}</p>
<p>{copyState}</p>
<button onClick={handleClick}>click</button>
</>
);
}
一開始可能會有點困惑,但這是 React functional component 的精髓,以這個例子來說會這樣運作:
當 React 首次 render 這個 component 時,此時的 state
是預設值 [1,2,3]
,copyState
也是 [1,2,3]
, 所以會得到像這樣的 React element
當我們點擊 button 時會呼叫 setState
觸發資料更新,首先 React 會以 Object.is([1,2,3], [4,5,6])
來檢查 state
是否有變更
- 由於比較結果是
false
,所以會觸發這個 component 的 re-render - 重新執行這個 component function。重新執行後的
state
就是更新後的[4,5,6](第一次更新)
; 而函式中setCopy(state)
在初始 scope 裡的state
是[1,2,3]
所以copyState
還是為[1,2,3]
- 再點擊一次 button ,React 以
Object.is([4,5,6], [4,5,6])
來檢查資料是否有變更。 - 比較結果是
false
( Array is by reference 所以雖然都是[4,5,6]
但他們是不同的 ),所以觸發 component re-render - 重新執行這個 component function, state 從
[4,5,6](第一次更新)
更新為[4,5,6](第二次更新)
,而copyState
也更新為[4,5,6](第一次更新)
React 不會去監聽資料的變化,你必須自己主動告知 React(也就是呼叫 setState
) 有資料需要更新並觸發 re-render。
在任何一次 render 裡面的 state
的值都並不會隨著時間或是呼叫 setState
而發生改變。而是每當我們呼叫 setState
時,React 會重新呼叫 component function 來重新執行一次 render。每次 render 時都會捕捉到屬於它自己版本的 state
值,這個值是個只存在於該次 render 中的常數。
每一次 render 都有自己的 effects
同理可證,每次 re-render 出來的 useEffect
也各自獨立,有這個概念後拿 BRE.dev 裏面考題應該也都得心應手,例如以下會印出什麼呢?
function App() {
const [state, setState] = useState(0)
console.log(state)
useEffect(() => {
setState(state => state + 1)
}, [])
useEffect(() => {
console.log(state)
setTimeout(() => {
console.log(state)
}, 100)
}, [])
return null
}
// 請停 3 秒
// 0
// 0
// 1
// 0
答對了嗎?你可以把它看成是
// 第一次 render
function App() {
const state = 0
console.log(state)
useEffect(() => {
// 觸發下一次 re-render
setState(state => state + 1)
}, [])
console.log(state)
setTimeout(() => {
console.log(state)
}, 100)
return null
}
// 0
// 0
// 第二次 render
function App() {
const state = 1
console.log(state)
return null
}
// 1
// 0 (第一次的setTimeout(() => {console.log(state)}, 100))
在 React 的管轄之下,真實 DOM Tree 會一直與 Virtual DOM Tree 保持結構一致。因此 React 只需要比較 re-render 前後的 Virtual DOM (Reconciliation),然後更新差異處到真實的 DOM Tree 就好
當我們呼叫一個 state 的 setState
方法時,React 就只會重繪該 state 所屬的 component 以及它底下的所有子孫 components,而不會整個 App 都重繪。
練習
推薦把 BRE.dev 中 render 跟 useEffect 題型都寫過一次,可以對 React re-render 運作更了解