大家好,我卡頌。
相比Vue
可以基於模版進行編譯時效能優化,React
作為一個完全執行時的庫,只能在執行時謀求效能優化。
這些優化對開發者大多是無感知的,但對專案進行效能優化時也常令開發者困惑。比如如下程式碼:
function App {
const [num, updateNum] = useState(0);
console.log('App render', num);
useEffect(() => {
setInterval(() => {
updateNum(1);
}, 1000)
}, [])
return <Child/>;
}
function Child() {
console.log('child render');
return <span>child</span>;
}
掛載App
元件後,會列印幾條資訊呢?
本文就這個Demo
講解React
內部的效能優化策略。
線上Demo地址
歡迎加入人類高質量前端框架群,帶飛
效能優化的效果
如果不考慮優化策略,程式碼執行邏輯如下:
App
元件首次render
,列印App render 0- 子元件
Child
首次render
,列印child render - 1000ms後,
setInterval
回撥觸發,執行updateNum(1)
App
元件再次render
,列印App render 1- 子元件
Child
再次render
,列印child render - 每過1000ms,重複步驟3~5
實際我們會發現,重複執行步驟3~5不會產生任何變化,這裡顯然是有優化空間的。
針對這種情況,React
確實做了優化。上述Demo
會依次列印:
- App render 0
- child render
- App render 1
- child render
- App render 1
這裡讓人困惑的點在於:為什麼num
從0變為1後,App render 1
執行了2次,而child render
只執行了一次?
接下來,我們從理論和實際角度解釋以上原因。
效能優化的理論
在useState文件中提到了一個名詞:bailout。
他指:當useState
更新的state
與當前state
一樣時(使用Object.is
比較),React
不會render
該元件的子孫元件。
注意:當命中bailout
後,當前元件可能還是會render
,只是他的子孫元件不會render
。
這是因為,大部分情況下,只有當前元件render
,useState
才會執行,才能計算出state
,進而與當前state
比較。
就我們的Demo
來說,只有App render
,useState
執行後才能計算出num
:
function App {
// useState執行後才能計算出num
const [num, updateNum] = useState(0);
// ...省略
}
在useState not bailing out when state does not change #14994中,Dan
也反覆強調這一觀點。
那麼從理論看,在我們的Demo
中,num
從0變為1後,child render只執行了一次是可以理解的,因為App
命中了bailout
,則他的子元件Child
不會render
。
但是bailout
只針對目標元件的子孫元件,那為什麼對於目標元件App
來說,App render 1
執行了2次後就不再執行了呢?
實際的效能優化策略,還要更復雜些。
實際的效能優化策略
React
的工作流程可以簡單概括為:
- 互動(比如
點選事件
、useEffect
)觸發更新 - 元件樹
render
剛才講的bailout
發生在步驟2:元件樹開始render
後,命中了bailout
的元件的子孫元件不會render
。
實際還有一種更前置的優化策略:當步驟1觸發更新時,發現state
未變化,則根本不會繼續步驟2。
從我們的Demo
來說:
function App {
const [num, updateNum] = useState(0);
console.log('App render', num);
useEffect(() => {
setInterval(() => {
updateNum(1);
}, 1000)
}, [])
return <Child/>;
}
正常情況,updateNum(1)
執行,觸發更新。直到App render
,useState
執行後才會計算出新的num
,進而與當前的num
比較,判斷是否命中bailout
。
如果updateNum(1)
執行後,立刻計算出新的num
,進而與當前的num
比較,如果相等則元件樹都不會render
。
這種將計算state的時機提前的策略,叫eagerState
(急切的state
)。
總結
綜上所述,我們的Demo
是混合了這兩種優化策略後的結果:
- App render 0(未命中策略)
- child render
- App render 1(未命中策略)
- child render
- App render 1(命中
bailout
) - (命中
eagerState
) - (命中
eagerState
)
......
bailout
的實現細節參考React元件到底什麼時候render啊。
限於篇幅有限,eagerState
的實現細節會單開一篇文章討論。