1. 程式人生 > 其它 >React的useLayoutEffect和useEffect執行時機有什麼不同

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?

流程

  1. react 在 diff 後,會進入到 commit 階段,準備把虛擬 DOM 發生的變化對映到真實 DOM 上

  2. 在 commit 階段的前期,會呼叫一些生命週期方法,對於類元件來說,需要觸發元件的 getSnapshotBeforeUpdate 生命週期,對於函式元件,此時會排程 useEffect 的 create destroy 函式

  3. 注意是排程,不是執行。在這個階段,會把使用了 useEffect 元件產生的生命週期函式入列到 React 自己維護的排程佇列中,給予一個普通的優先順序,讓這些生命週期函式非同步執行

// 可以近似的認為,React 做了這樣一步,實際流程中要複雜的多

setTimeout(() => {
      const preDestory = element.destroy;
      if (!preDestory) prevDestroy();
      const destroy = create();
      element.destroy= destroy;
}, 0);
  1. 隨後,就到了 React 把虛擬 DOM 設定到真實 DOM 上的階段,這個階段主要呼叫的函式是 commitWork,commitWork 函式會針對不同的 fiber 節點呼叫不同的 DOM 的修改方法,比如文字節點和元素節點的修改方法是不一樣的。

  2. commitWork 如果遇到了類元件的 fiber 節點,不會做任何操作,會直接 return,進行收尾工作,然後去處理下一個節點,這點很容易理解,類元件的 fiber 節點沒有對應的真實 DOM 結構,所以就沒有相關操作

  3. 但在有了 hooks 以後,函式元件在這個階段,會同步呼叫上一次渲染時 useLayoutEffect(create, deps) create 函式返回的 destroy 函式

  4. 注意一個節點在 commitWokr 後,這個時候,我們已經把發生的變化對映到真實 DOM 上了

  5. 但由於 JS 執行緒和瀏覽器渲染執行緒是互斥的,因為 JS 虛擬機器還在執行,即使記憶體中的真實 DOM 已經變化,瀏覽器也沒有立刻渲染到螢幕上

  6. 此時會進行收尾工作,同步執行對應的生命週期方法,我們說的componentDidMount,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函式都是在這個階段被同步執行

  7. 對於 react 來說,commit 階段是不可打斷的,會一次性把所有需要 commit 的節點全部 commit 完,至此 react 更新完畢,JS 停止執行

  8. 瀏覽器把發生變化的 DOM 渲染到螢幕上,到此為止 react 僅用一次迴流、重繪的代價,就把所有需要更新的 DOM 節點全部更新完成

  9. 瀏覽器渲染完成後,瀏覽器通知 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 進行修改,會觸發瀏覽器再次進行迴流、重繪,增加了效能上的損耗。