我們先看下 React 官方文檔對(duì)這兩個(gè) hook 的介紹,建立個(gè)整體認(rèn)識(shí)
useEffect(create, deps):
該 Hook 接收一個(gè)包含命令式,、且可能有副作用代碼的函數(shù),。在函數(shù)組件主體內(nèi)(這里指在 React 渲染階段)改變 DOM、添加訂閱,、設(shè)置定時(shí)器、記錄日志以及執(zhí)行其他包含副作用的操作都是不被允許的,,因?yàn)檫@可能會(huì)產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性,。使用 useEffect 完成副作用操作。賦值給 useEffect 的函數(shù)會(huì)在組件渲染到屏幕之后執(zhí)行,。你可以把 effect 看作從 React 的純函數(shù)式世界通往命令式世界的逃生通道,。
useLayoutEffect(create, deps):
其函數(shù)簽名與 useEffect 相同,但它會(huì)在所有的 DOM 變更之后同步調(diào)用 effect,??梢允褂盟鼇碜x取 DOM 布局并同步觸發(fā)重渲染。在瀏覽器執(zhí)行繪制之前,,useLayoutEffect 內(nèi)部的更新計(jì)劃將被同步刷新,。
注意加粗的字段,,React 官方的文檔其實(shí)把兩個(gè) hook 的執(zhí)行時(shí)機(jī)說的很清楚,下面我們深入到 react 的執(zhí)行流程中來理解下
問題
- useEffect 和 useLayoutEffect 的區(qū)別,?
- useEffect 和 useLayoutEffect 哪一個(gè)與 componentDidMount,,componentDidUpdate 的是等價(jià)的?
- useEffect 和 useLayoutEffect 哪一個(gè)與 componentWillUnmount 的是等價(jià)的,?
- 為什么建議將修改 DOM 的操作里放到 useLayoutEffect 里,,而不是 useEffect?
流程
-
react 在 diff 后,,會(huì)進(jìn)入到 commit 階段,,準(zhǔn)備把虛擬 DOM 發(fā)生的變化映射到真實(shí) DOM 上
-
在 commit 階段的前期,會(huì)調(diào)用一些生命周期方法,,對(duì)于類組件來說,,需要觸發(fā)組件的 getSnapshotBeforeUpdate 生命周期,對(duì)于函數(shù)組件,,此時(shí)會(huì)調(diào)度 useEffect 的 create destroy 函數(shù)
-
注意是調(diào)度,,不是執(zhí)行。在這個(gè)階段,,會(huì)把使用了 useEffect 組件產(chǎn)生的生命周期函數(shù)入列到 React 自己維護(hù)的調(diào)度隊(duì)列中,,給予一個(gè)普通的優(yōu)先級(jí),讓這些生命周期函數(shù)異步執(zhí)行
// 可以近似的認(rèn)為,,React 做了這樣一步,,實(shí)際流程中要復(fù)雜的多
setTimeout(() => {
const preDestory = element.destroy;
if (!preDestory) prevDestroy();
const destroy = create();
element.destroy= destroy;
}, 0);
-
隨后,就到了 React 把虛擬 DOM 設(shè)置到真實(shí) DOM 上的階段,,這個(gè)階段主要調(diào)用的函數(shù)是 commitWork,,commitWork 函數(shù)會(huì)針對(duì)不同的 fiber 節(jié)點(diǎn)調(diào)用不同的 DOM 的修改方法,比如文本節(jié)點(diǎn)和元素節(jié)點(diǎn)的修改方法是不一樣的,。
-
commitWork 如果遇到了類組件的 fiber 節(jié)點(diǎn),,不會(huì)做任何操作,會(huì)直接 return,,進(jìn)行收尾工作,,然后去處理下一個(gè)節(jié)點(diǎn),這點(diǎn)很容易理解,,類組件的 fiber 節(jié)點(diǎn)沒有對(duì)應(yīng)的真實(shí) DOM 結(jié)構(gòu),,所以就沒有相關(guān)操作
-
但在有了 hooks 以后,函數(shù)組件在這個(gè)階段,,會(huì)同步調(diào)用上一次渲染時(shí) useLayoutEffect(create, deps) create 函數(shù)返回的 destroy 函數(shù)
-
注意一個(gè)節(jié)點(diǎn)在 commitWokr 后,,這個(gè)時(shí)候,我們已經(jīng)把發(fā)生的變化映射到真實(shí) DOM 上了
-
但由于 JS 線程和瀏覽器渲染線程是互斥的,,因?yàn)?JS 虛擬機(jī)還在運(yùn)行,,即使內(nèi)存中的真實(shí) DOM 已經(jīng)變化,,瀏覽器也沒有立刻渲染到屏幕上
-
此時(shí)會(huì)進(jìn)行收尾工作,同步執(zhí)行對(duì)應(yīng)的生命周期方法,,我們說的componentDidMount,,componentDidUpdate 以及 useLayoutEffect(create, deps) 的 create 函數(shù)都是在這個(gè)階段被同步執(zhí)行。
-
對(duì)于 react 來說,,commit 階段是不可打斷的,,會(huì)一次性把所有需要 commit 的節(jié)點(diǎn)全部 commit 完,至此 react 更新完畢,,JS 停止執(zhí)行
-
瀏覽器把發(fā)生變化的 DOM 渲染到屏幕上,,到此為止 react 僅用一次回流、重繪的代價(jià),,就把所有需要更新的 DOM 節(jié)點(diǎn)全部更新完成
-
瀏覽器渲染完成后,,瀏覽器通知 react 自己處于空閑階段,react 開始執(zhí)行自己調(diào)度隊(duì)列中的任務(wù),,此時(shí)才開始執(zhí)行 useEffect(create, deps) 的產(chǎn)生的函數(shù)
幾個(gè)問題
useEffect 和 useLayoutEffect 的區(qū)別?
useEffect 在渲染時(shí)是異步執(zhí)行,,并且要等到瀏覽器將所有變化渲染到屏幕后才會(huì)被執(zhí)行,。
useLayoutEffect 在渲染時(shí)是同步執(zhí)行,其執(zhí)行時(shí)機(jī)與 componentDidMount,,componentDidUpdate 一致
對(duì)于 useEffect 和 useLayoutEffect 哪一個(gè)與 componentDidMount,,componentDidUpdate 的是等價(jià)的,?
useLayoutEffect,因?yàn)閺脑创a中調(diào)用的位置來看,,useLayoutEffect的 create 函數(shù)的調(diào)用位置,、時(shí)機(jī)都和 componentDidMount,componentDidUpdate 一致,,且都是被 React 同步調(diào)用,,都會(huì)阻塞瀏覽器渲染。
useEffect 和 useLayoutEffect 哪一個(gè)與 componentWillUnmount 的是等價(jià)的?
同上,,useLayoutEffect 的 detroy 函數(shù)的調(diào)用位置,、時(shí)機(jī)與 componentWillUnmount 一致,且都是同步調(diào)用,。useEffect 的 detroy 函數(shù)從調(diào)用時(shí)機(jī)上來看,,更像是 componentDidUnmount (注意React 中并沒有這個(gè)生命周期函數(shù)),。
為什么建議將修改 DOM 的操作里放到 useLayoutEffect 里,,而不是 useEffect?
可以看到在流程9/10期間,,DOM 已經(jīng)被修改,,但但瀏覽器渲染線程依舊處于被阻塞階段,所以還沒有發(fā)生回流,、重繪過程,。由于內(nèi)存中的 DOM 已經(jīng)被修改,通過 useLayoutEffect 可以拿到最新的 DOM 節(jié)點(diǎn),,并且在此時(shí)對(duì) DOM 進(jìn)行樣式上的修改,,假設(shè)修改了元素的 height,這些修改會(huì)在步驟 11 和 react 做出的更改一起被一次性渲染到屏幕上,,依舊只有一次回流,、重繪的代價(jià)。
如果放在 useEffect 里,,useEffect 的函數(shù)會(huì)在組件渲染到屏幕之后執(zhí)行,,此時(shí)對(duì) DOM 進(jìn)行修改,會(huì)觸發(fā)瀏覽器再次進(jìn)行回流,、重繪,,增加了性能上的損耗。
|