我們先看下 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 進行修改,會觸發瀏覽器再次進行回流、重繪,增加了性能上的損耗,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/73781.html
標籤:JavaScript
上一篇:JS 異步與 Promise
下一篇:一窺 AJAX
