React的useLayoutEffect和useEffect執行時機有什麼不同
我們先看下 React 官方文件對這兩個 hook 的介紹,建立個整體認識
useEffect(create, deps):
該 Hook 接收一個包含命令式、且可能有副作用程式碼的函式。在函式元件主體內(這裡指在 React 渲染階段)改變 DOM、新增訂閱、設定定時器、記錄日誌以及執行其他包含副作用的操作都是不被允許的,因為這可能會產生莫名其妙的 bug 並破壞 UI 的一致性。使用 useEffect 完成副作用操作。賦值給 useEffect 的函式會在元件渲染到螢幕之後執行。你可以把 effect 看作從 React 的純函式式世界通往命令式世界的逃生通道。
useLayoutEffect(create, deps):
其函式簽名與 useEffect 相同,但它會在所有的 DOM 變更之後同步呼叫 effect。可以使用它來讀取 DOM 佈局並同步觸發重渲染。在瀏覽器執行繪製之前,useLayoutEffect 內部的更新計劃將被同步重新整理。
注意加粗的欄位,React 官方的文件其實把兩個 hook 的執行時機說的很清楚,下面我們深入到 react 的執行流程中來理解下
問題
- useEffect 和 useLayoutEffect 的區別?
- useEffect 和 useLayoutEffect 哪一個與 componentDidMount,componentDidUpdate 的是等價的?
- useEffect 和 useLayoutEffect 哪一個與 componentWillUnmount 的是等價的?
- 為什麼建議將修改 DOM 的操作裡放到 useLayoutEffect 裡,而不是 useEffect?
流程
-
react 在 diff 後,會進入到 commit 階段,準備把虛擬 DOM 發生的變化對映到真實 DOM 上
-
在 commit 階段的前期,會呼叫一些生命週期方法,對於類元件來說,需要觸發元件的 getSnapshotBeforeUpdate 生命週期,對於函式元件,此時會排程 useEffect 的 create destroy 函式
-
注意是排程,不是執行。在這個階段,會把使用了 useEffect 元件產生的生命週期函式入列到 React 自己維護的排程佇列中,給予一個普通的優先順序,讓這些生命週期函式非同步執行
// 可以近似的認為,React 做了這樣一步,實際流程中要複雜的多
setTimeout(() => {
const preDestory = element.destroy;
if (!preDestory) prevDestroy();
const destroy = create();
element.destroy= destroy;
}, 0);
-
隨後,就到了 React 把虛擬 DOM 設定到真實 DOM 上的階段,這個階段主要呼叫的函式是 commitWork,commitWork 函式會針對不同的 fiber 節點呼叫不同的 DOM 的修改方法,比如文字節點和元素節點的修改方法是不一樣的。
-
commitWork 如果遇到了類元件的 fiber 節點,不會做任何操作,會直接 return,進行收尾工作,然後去處理下一個節點,這點很容易理解,類元件的 fiber 節點沒有對應的真實 DOM 結構,所以就沒有相關操作
-
但在有了 hooks 以後,函式元件在這個階段,會同步呼叫上一次渲染時 useLayoutEffect(create, deps) create 函式返回的 destroy 函式
-
注意一個節點在 commitWokr 後,這個時候,我們已經把發生的變化對映到真實 DOM 上了
-
但由於 JS 執行緒和瀏覽器渲染執行緒是互斥的,因為 JS 虛擬機器還在執行,即使記憶體中的真實 DOM 已經變化,瀏覽器也沒有立刻渲染到螢幕上
-
此時會進行收尾工作,同步執行對應的生命週期方法,我們說的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函式都是在這個階段被同步執行。
-
對於 react 來說,commit 階段是不可打斷的,會一次性把所有需要 commit 的節點全部 commit 完,至此 react 更新完畢,JS 停止執行
-
瀏覽器把發生變化的 DOM 渲染到螢幕上,到此為止 react 僅用一次迴流、重繪的代價,就把所有需要更新的 DOM 節點全部更新完成
-
瀏覽器渲染完成後,瀏覽器通知 react 自己處於空閒階段,react 開始執行自己排程佇列中的任務,此時才開始執行 useEffect(create, deps) 的產生的函式
解答
useEffect 和 useLayoutEffect 的區別?
useEffect 在渲染時是非同步執行,並且要等到瀏覽器將所有變化渲染到屏幕後才會被執行。
useLayoutEffect 在渲染時是同步執行,其執行時機與 componentDidMount,componentDidUpdate 一致
對於 useEffect 和 useLayoutEffect 哪一個與 componentDidMount,componentDidUpdate 的是等價的?
useLayoutEffect,因為從原始碼中呼叫的位置來看,useLayoutEffect的 create 函式的呼叫位置、時機都和 componentDidMount,componentDidUpdate 一致,且都是被 React 同步呼叫,都會阻塞瀏覽器渲染。參考 前端進階面試題詳細解答
useEffect 和 useLayoutEffect 哪一個與 componentWillUnmount 的是等價的?
同上,useLayoutEffect 的 detroy 函式的呼叫位置、時機與 componentWillUnmount 一致,且都是同步呼叫。useEffect 的 detroy 函式從呼叫時機上來看,更像是 componentDidUnmount (注意React 中並沒有這個生命週期函式)。
為什麼建議將修改 DOM 的操作裡放到 useLayoutEffect 裡,而不是 useEffect?
可以看到在流程9/10期間,DOM 已經被修改,但但瀏覽器渲染執行緒依舊處於被阻塞階段,所以還沒有發生迴流、重繪過程。由於記憶體中的 DOM 已經被修改,通過 useLayoutEffect 可以拿到最新的 DOM 節點,並且在此時對 DOM 進行樣式上的修改,假設修改了元素的 height,這些修改會在步驟 11 和 react 做出的更改一起被一次性渲染到螢幕上,依舊只有一次迴流、重繪的代價。
如果放在 useEffect 裡,useEffect 的函式會在元件渲染到螢幕之後執行,此時對 DOM 進行修改,會觸發瀏覽器再次進行迴流、重繪,增加了效能上的損耗。