Deep dive React re-render

React 從呼叫 setState 到畫面改變,中間發生了什麼事?

Hannah Lin
Hannah Lin
11 min readAug 25, 2024

--

Deep dive React re-render系列文

1. React Reconciliation
2. Deep dive React re-render

re-render process

就算寫 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/>
}
  1. setState 方法觸發資料更新
  2. 此時 React 會先以 Object.is() 方法來檢查新舊 state 是否不同,如果相同的話則判定資料沒有變化所以畫面不用更新,就會直接中斷接下來的流程。如果不同的話則代表資料有所變化,因此接續以下 re-render 流程
  3. 觸發 re-render 後,React 再次以新的 state 重新執行一次 Parent 這個 function,函式裡的所有東西都會被 re-create,不管理面的是函式、 <Child/>useEffect
  4. React Build a tree of Virtual DOM of <Child/> element
  5. 新舊 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

只要藉由 setStatestate 改變就會觸發 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.memouseMemo)

// 以下就算按無數次 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

每次 render 出來 function 彼此都沒有任何關聯,只是命名相同

每一次 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 運作更了解

--

--