官方解釋
官方解釋鸳粉,這兩個(gè)hook基本相同闷盔,調(diào)用時(shí)機(jī)不同弯洗,請(qǐng)全部使用useEffect,除非遇到bug或者不可解決的問題逢勾,再考慮使用useLayoutEffect。還舉了個(gè)例子藐吮,譬如你想測(cè)量DOM元素時(shí)候溺拱,使用useLayoutEffect。個(gè)人感覺舉例不恰當(dāng)谣辞,測(cè)試DOM我也完全可以在useEffect中測(cè)量啊迫摔。說(shuō)如果需要在paint前改變DOM,更合適泥从。
我做過(guò)測(cè)試句占,譬如一個(gè)div尺寸是200 * 200,我想改成100 * 100躯嫉,如果寫在useEffect中纱烘,確實(shí)會(huì)造成頁(yè)面抖動(dòng)杨拐,寫在useLayoutEffect中可以避免。
redux-react-hook 中的妙用
redux-react-hook庫(kù)中有段代碼使用了useLayoutEffect擂啥,用來(lái)避免組件render兩次哄陶。
這里的useIsomorphicLayoutEffect就是useLayoutEffect(因?yàn)閹?kù)要區(qū)分是瀏覽器還是SSR,所以上面做了處理)
// We use useLayoutEffect to render once if we have multiple useMappedState.
// We need to update lastStateRef synchronously after rendering component,
// With useEffect we would have:
// 1) dispatch action
// 2) call subscription cb in useMappedState1, call forceUpdate
// 3) rerender component
// 4) call useMappedState1 and useMappedState2 code
// 5) calc new derivedState in useMappedState2, schedule updating lastStateRef, return new state, render component
// 6) call subscription cb in useMappedState2, check if lastStateRef !== newDerivedState, call forceUpdate, rerender.
// 7) update lastStateRef - it's too late, we already made one unnecessary render
useIsomorphicLayoutEffect(() => {
lastStateRef.current = derivedState;
memoizedMapStateRef.current = memoizedMapState;
});
看得很懵逼哺壶,講了如果用useEffect會(huì)帶來(lái)什么問題屋吨,我模擬了很久終于模擬出來(lái)作者描述的問題(意圖好猜,模擬時(shí)候有個(gè)細(xì)節(jié)很難處理)
模擬場(chǎng)景簡(jiǎn)化
我有一個(gè)數(shù)據(jù)store(對(duì)redux的store)山宾,一個(gè)組件App至扰,組件中使用了useA和useB兩個(gè)自定義hook(這對(duì)應(yīng)兩次調(diào)用redux-react-hook的useMappedState)。
當(dāng)我一個(gè)操作资锰,改變store時(shí)候敢课,去調(diào)用訂閱者即A和B,A和B改變會(huì)觸發(fā)App重新render台妆。這里有個(gè)問題翎猛,A和B都是訂閱者,會(huì)觸發(fā)兩次App重新render接剩,作者想避免切厘,所以會(huì)在use的時(shí)候做下處理,使用useEffect的話懊缺,會(huì)出現(xiàn)bug疫稿,無(wú)法如愿,下面就來(lái)模擬這個(gè)過(guò)程鹃两。
代碼實(shí)現(xiàn)
function App() {
console.log('%c App render--start-->', 'color:blue')
const a = useA();
const b = useB();
function doSomething() {
// dispatch();
setTimeout(dispatch, 0)
}
console.log('%c App render--end-->', 'color:red')
return (
<div>
<p>a: {a}</p>
<p>b: 遗座</p>
<p><button onClick={doSomething}>dispatch</button></p>
</div>
)
}
function useA() {
console.log('---a--hook-->')
const [trigger, setTrigger] = useState(0);
useEffect(() => {
console.log('--useA--useEffect-->')
memoStore = store;
});
useEffect(() => {
const fn = subsriber(() => {
console.log('--useA--注冊(cè)函數(shù)--->', memoStore, store);
if(store !== memoStore) {
setTrigger(Math.random())
}
});
return () => unSubsriber(fn);
}, []);
return store;
}
function useB() {
console.log('---b--hook-->')
const [trigger, setTrigger] = useState(0);
useEffect(() => {
console.log('--useA--useEffect-->')
memoStore = store
});
useEffect(() => {
const fn = subsriber(() => {
console.log('--useB--注冊(cè)函數(shù)--->', memoStore, store);
if(store !== memoStore) {
setTrigger(Math.random())
}
});
return () => unSubsriber(fn);
}, []);
return store;
}
簡(jiǎn)化的redux:
let store = 6;
let memoStore = 6;
const newStore = 8;
const subsriberList = new Set();
function subsriber(fn) {
subsriberList.add(fn);
return fn;
}
function unSubsriber(fn) {
subsriberList.delete(fn)
}
function dispatch() {
memoStore = store;
store = newStore;
subsriberList.forEach(fn => fn())
}
這里有一個(gè)非常重要的關(guān)鍵點(diǎn),就是App組件中的doSometing中俊扳,dispatch一定要寫在setTimeout中途蒋,否則react自動(dòng)幫你優(yōu)化了,模擬不出來(lái)想要的場(chǎng)景馋记。
分析
點(diǎn)擊按鈕時(shí)候号坡,改變了store: 6 -> 8,觸發(fā)了訂閱者自定義hook A和B的訂閱事件梯醒。按理會(huì)觸發(fā)兩次App render宽堆,但是我們做了優(yōu)化,在useA和useB的時(shí)候茸习,會(huì)用新狀態(tài)去覆蓋舊狀態(tài)畜隶,然后在訂閱事件中,會(huì)對(duì)比新老狀態(tài),一致的話籽慢,就不去觸發(fā)自定義hook改變了浸遗,也就不會(huì)觸發(fā)App render了。
但是使用effect的話嗡综,實(shí)際執(zhí)行過(guò)程是這樣的:
可以看到乙帮,App依舊render了兩次,其中主要問題就出在useEffect注冊(cè)的函數(shù)在什么時(shí)候執(zhí)行极景,從流程圖中可以看到察净,其不是在App組件樹 render結(jié)束后立即執(zhí)行的(我也不知道什么時(shí)候執(zhí)行,還請(qǐng)哪位大佬指點(diǎn))盼樟,js會(huì)繼續(xù)執(zhí)行后面的代碼(B的訂閱)氢卡,這個(gè)時(shí)候old=new還沒有執(zhí)行,所以依舊觸發(fā)了第二次App組件render晨缴。
更改useEffect為useLayoutEffect
useA
...
useLayoutEffect(() => {
console.log('--useA--useLayoutEffect-->')
memoStore = store;
});
...
useB
...
useLayoutEffect(() => {
console.log('--useA--useLayoutEffect-->')
memoStore = store
});
...
可以看見關(guān)鍵點(diǎn)是译秦,layoutEffect隊(duì)列在組件樹render結(jié)束后,會(huì)立刻同步執(zhí)行(個(gè)人感覺是的)击碗,所以在第一次App render結(jié)束后筑悴,old和new就相同了,在執(zhí)行B訂閱時(shí)候稍途,就會(huì)根據(jù)條件阁吝,不再觸發(fā)App render了。
總結(jié)
// 一定要加setTimeout模擬異步操作械拍,否則實(shí)驗(yàn)不出來(lái)上面的流程的
setTimeout(()=>{
renderApp1(); // 一些會(huì)條件性觸發(fā)組件重新render的代碼
exeLayoutEffectList(); // 組件樹構(gòu)建完畢突勇,會(huì)同步執(zhí)行useLayoutEffect中的代碼
code1(); // 一些js代碼
code2(); // 一些js代碼
// 所有代碼都執(zhí)行完畢后,瀏覽器渲染結(jié)束后坷虑,會(huì)調(diào)用useEffect中的代碼
// 或者接到下一次組件刷新(re-render)指令甲馋,會(huì)將上一次effect隊(duì)列執(zhí)行完畢。我根據(jù)試驗(yàn)猜的
exeEffectList();
renderApp2(); // 一些會(huì)條件性觸發(fā)組件重新render的代碼
}, 0)
主要就是effect和layoutEffect隊(duì)列的執(zhí)行階段迄损,layout會(huì)在組件樹構(gòu)建完畢或者刷新完畢后同步立刻執(zhí)行定躏。effect會(huì)等其他js代碼執(zhí)行完畢后執(zhí)行(或者遇到下一次刷新任務(wù)前)
回過(guò)頭再看react關(guān)于useLayoutEffect的官方文檔:
The signature is identical to useEffect, but it fires synchronously after all DOM mutations. Use this to read layout from the DOM and synchronously re-render. Updates scheduled inside useLayoutEffect will be flushed synchronously, before the browser has a chance to paint.
Prefer the standard useEffect when possible to avoid blocking visual updates.
- 和useEffect相同,是指他們都在組件樹構(gòu)建完畢之后執(zhí)行的
- 但是useLayout是在DOM突變之后立即執(zhí)行的芹敌,突變是指什么共屈?是指類似組件構(gòu)建完畢之后,appendChild(reactTree)這種操作嗎?
- 可以肯定的是党窜,是在組件樹構(gòu)建完畢后同步執(zhí)行,之后才會(huì)去執(zhí)行后面的js代碼
- 使用他來(lái)讀取DOM布局尺寸借宵,我倒感覺應(yīng)該是寫成設(shè)定DOM布局尺寸幌衣,這樣可以防抖動(dòng),同步讀取DOM布局尺寸想不懂有什么用
- useLayoutEffect隊(duì)列中的任務(wù),會(huì)在瀏覽器paint之前執(zhí)行(可以用來(lái)防抖)
- 盡可能使用useEffect來(lái)避免阻塞視覺更新(見上條豁护,阻礙paint)
吐槽
英語(yǔ)太差哼凯,好多概念模模糊糊的,但是好像看過(guò)國(guó)外文章楚里,也有吐槽react的幾個(gè)概念含糊不清的断部。