我們先看下 React 官方文檔對這兩個 hook 的介紹,建立個整體認識 useEffect(create, deps): 該 Hook 接收一個包含命令式、且可能有副作用代碼的函數。在函數組件主體內(這裡指在 React 渲染階段)改變 DOM、添加訂閱、設置定時器、記錄日誌以及執行其他包含副作 ...
我們先看下 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 進行修改,會觸發瀏覽器再次進行迴流、重繪,增加了性能上的損耗。