useEffect和useLayoutEffect的原理與區(qū)別
本文適合對React的use(Layout)Effect有疑惑的小伙伴閱讀。
歡迎關(guān)注前端早茶,與廣東靚仔共同進階~
一、前言
二、正文
Effect的數(shù)據(jù)結(jié)構(gòu)
對函數(shù)組件來說,其fiber上的memorizedState專門用來存儲hooks鏈表,每一個hook對應(yīng)鏈表中的每一個元素。
useEffect/useLayoutEffect產(chǎn)生的hook會放到fiber.memorizedState上,而它們調(diào)用后最終會生成一個effect對象,存儲到它們對應(yīng)hook的memoizedState中,與其他的effect連接成環(huán)形鏈表。
單個的effect對象包括以下幾個屬性:
create: 傳入use(Layout)Effect函數(shù)的第一個參數(shù),即回調(diào)函數(shù) destroy: 回調(diào)函數(shù)return的函數(shù),在該effect銷毀的時候執(zhí)行 deps: 依賴項 next: 指向下一個effect tag: effect的類型,區(qū)分是useEffect還是useLayoutEffect
單純看effect對象中的字段,很容易和平時的用法聯(lián)系起來。create函數(shù)即我們傳入useEffect/useLayoutEffect的回調(diào)函數(shù),而通過deps,可以控制create是否執(zhí)行,如需清除effect,則在create函數(shù)中return一個新函數(shù)(即destroy)即可。
為了理解effect的數(shù)據(jù)結(jié)構(gòu),假設(shè)有如下組件:
const?UseEffectExp?=?()?=>?{
????const?[?text,?setText?]?=?useState('hello')
????useEffect(()?=>?{
????????console.log('effect1')
????????return?()?=>?{
????????????console.log('destory1');
????????}
????})
????useLayoutEffect(()?=>?{
????????console.log('effect2')
????????return?()?=>?{
????????????console.log('destory2');
????????}
????})
????return?<div>effectdiv>
}掛載到它fiber上memoizedState的hooks鏈表結(jié)構(gòu)如下

例如useEffect hook上的memoizedState存儲了useEffect 的 effect對象(effect1),next指向useLayoutEffect的effect對象(effect2)。effect2的next又指回effect1.在下面的useLayoutEffect hook中,也是如此的結(jié)構(gòu)。

effect除了保存在fiber.memoizedState對應(yīng)的hook中,還會保存在fiber的updateQueue中。

現(xiàn)在,我們知道,調(diào)用use(Layout)Effect,最后會產(chǎn)生effect鏈表,這個鏈表保存在兩個地方:
fiber.memoizedState的hooks鏈表中,use(Layout)Effect對應(yīng)hook元素的memoizedState中。
fiber.updateQueue中,本次更新的updateQueue,它會在本次更新的commit階段中被處理。
流程概述
render階段:函數(shù)組件開始渲染的時候,創(chuàng)建出對應(yīng)的hook鏈表掛載到 workInProgress的 memoizedState上,并創(chuàng)建effect鏈表,但是基于上次和本次依賴項的比較結(jié)果, 創(chuàng)建的effect是有差異的。 這一點暫且可以理解為:依賴項有變化,effect可以被處理,否則不會被處理。 commit階段:異步調(diào)度useEffect,layout階段同步處理useLayoutEffect的 effect。等到commit階段完成,更新應(yīng)用到頁面上之后,開始處理useEffect 產(chǎn)生的effect。
commit階段新的DOM準(zhǔn)備完成,但還未渲染到屏幕之前,同步執(zhí)行。三、實現(xiàn)細節(jié)
通過整體流程可以看出,effect的整個過程涉及到render階段和commit階段。render階段只創(chuàng)建effect鏈表,commit階段去處理這個鏈表。所有實現(xiàn)的細節(jié)都是在圍繞effect鏈表。
render階段-創(chuàng)建effect鏈表
在實際的使用中,我們調(diào)用的use(Layout)Effect函數(shù),在掛載和更新的過程是不同的。
掛載時,調(diào)用的是mountEffectImpl,它會為use(Layout)Effect這類hook創(chuàng)建一個hook對象,將workInProgressHook指向它,然后在這個fiber節(jié)點的flag中加入副作用相關(guān)的effectTag。最后,會構(gòu)建effect鏈表掛載到fiber的updateQueue,并且也會在hook上的memorizedState掛載effect。
function?mountEffectImpl(fiberFlags,?hookFlags,?create,?deps):?void?{
??//?創(chuàng)建hook對象
??const?hook?=?mountWorkInProgressHook();
??//?獲取依賴
??const?nextDeps?=?deps?===?undefined???null?:?deps;
??//?為fiber打上副作用的effectTag
??currentlyRenderingFiber.flags?|=?fiberFlags;
??//?創(chuàng)建effect鏈表,掛載到hook的memoizedState上和fiber的updateQueue
??hook.memoizedState?=?pushEffect(
????HookHasEffect?|?hookFlags,
????create,
????undefined,
????nextDeps,
??);
}
currentlyRenderingFiber 即 workInProgress節(jié)點
更新時,調(diào)用updateEffectImpl,完成effect鏈表的構(gòu)建。這個過程中會根據(jù)前后依賴項是否變化,從而創(chuàng)建不同的effect對象。具體體現(xiàn)在effect的tag上,如果前后依賴未變,則effect的tag就賦值為傳入的hookFlags,否則,在tag中加入HookHasEffect標(biāo)志位。正是因為這樣,在處理effect鏈表時才可以只處理依賴變化的effect,use(Layout)Effect可以根據(jù)它的依賴變化情況來決定是否執(zhí)行回調(diào)。
function?updateEffectImpl(fiberFlags,?hookFlags,?create,?deps):?void?{
??const?hook?=?updateWorkInProgressHook();
??const?nextDeps?=?deps?===?undefined???null?:?deps;
??let?destroy?=?undefined;
??if?(currentHook?!==?null)?{
????//?從currentHook中獲取上一次的effect
????const?prevEffect?=?currentHook.memoizedState;
????//?獲取上一次effect的destory函數(shù),也就是useEffect回調(diào)中return的函數(shù)
????destroy?=?prevEffect.destroy;
????if?(nextDeps?!==?null)?{
??????const?prevDeps?=?prevEffect.deps;
??????//?比較前后依賴,push一個不帶HookHasEffect的effect
??????if?(areHookInputsEqual(nextDeps,?prevDeps))?{
????????pushEffect(hookFlags,?create,?destroy,?nextDeps);
????????return;
??????}
????}
??}
??currentlyRenderingFiber.flags?|=?fiberFlags;
??//?如果前后依賴有變,在effect的tag中加入HookHasEffect
??//?并將新的effect更新到hook.memoizedState上
??hook.memoizedState?=?pushEffect(
????HookHasEffect?|?hookFlags,
????create,
????destroy,
????nextDeps,
??);
}
在組件掛載和更新時,有一個區(qū)別,就是掛載期間調(diào)用pushEffect創(chuàng)建effect對象的時候并沒有傳destroy函數(shù),而更新期間傳了,這是因為每次effect執(zhí)行時,都是先執(zhí)行前一次的銷毀函數(shù),再執(zhí)行新effect的創(chuàng)建函數(shù)。而掛載期間,上一次的effect并不存在,執(zhí)行創(chuàng)建函數(shù)前也就無需先銷毀。
function?pushEffect(tag,?create,?destroy,?deps)?{
??//?創(chuàng)建effect對象
??const?effect:?Effect?=?{
????tag,
????create,
????destroy,
????deps,
????//?Circular
????next:?(null:?any),
??};
??//?從workInProgress節(jié)點上獲取到updateQueue,為構(gòu)建鏈表做準(zhǔn)備
??let?componentUpdateQueue:?null?|?FunctionComponentUpdateQueue?=?(currentlyRenderingFiber.updateQueue:?any);
??if?(componentUpdateQueue?===?null)?{
????//?如果updateQueue為空,把effect放到鏈表中,和它自己形成閉環(huán)
????componentUpdateQueue?=?createFunctionComponentUpdateQueue();
????//?將updateQueue賦值給WIP節(jié)點的updateQueue,實現(xiàn)effect鏈表的掛載
????currentlyRenderingFiber.updateQueue?=?(componentUpdateQueue:?any);
????componentUpdateQueue.lastEffect?=?effect.next?=?effect;
??}?else?{
????//?updateQueue不為空,將effect接到鏈表的后邊
????const?lastEffect?=?componentUpdateQueue.lastEffect;
????if?(lastEffect?===?null)?{
??????componentUpdateQueue.lastEffect?=?effect.next?=?effect;
????}?else?{
??????const?firstEffect?=?lastEffect.next;
??????lastEffect.next?=?effect;
??????effect.next?=?firstEffect;
??????componentUpdateQueue.lastEffect?=?effect;
????}
??}
??return?effect;
}
函數(shù)組件和類組件的updateQueue都是環(huán)狀鏈表
以上,就是effect鏈表的構(gòu)建過程。我們可以看到,effect對象創(chuàng)建出來最終會以兩種形式放到兩個地方:單個的effect,放到hook.memorizedState上;環(huán)狀的effect鏈表,放到fiber節(jié)點的updateQueue中。
兩者各有用途,前者的effect會作為上次更新的effect,為本次創(chuàng)建effect對象提供參照(對比依賴項數(shù)組),后者的effect鏈表會作為最終被執(zhí)行的主體,帶到commit階段處理。
commit階段 effect如何被處理
useEffect的異步調(diào)度
scheduleCallback,將執(zhí)行useEffect的動作作為一個任務(wù)去調(diào)度,這個任務(wù)會異步調(diào)用。commit階段和useEffect真正扯上關(guān)系的有三個地方:commit階段的開始、beforeMutation、layout,涉及到異步調(diào)度的是后面兩個。function?commitRootImpl(root,?renderPriorityLevel)?{
??//?進入commit階段,先執(zhí)行一次之前未執(zhí)行的useEffect
??do?{
????flushPassiveEffects();
??}?while?(rootWithPendingPassiveEffects?!==?null);
??...
??do?{
????try?{
??????// beforeMutation階段的處理函數(shù):commitBeforeMutationEffects內(nèi)部,
??????//?異步調(diào)度useEffect
??????commitBeforeMutationEffects();
????}?catch?(error)?{
??????...
????}
??}?while?(nextEffect?!==?null);
??...
??const?rootDidHavePassiveEffects?=?rootDoesHavePassiveEffects;
??if?(rootDoesHavePassiveEffects)?{
????//?重點,記錄有副作用的effect
????rootWithPendingPassiveEffects?=?root;
??}
}commit開始,先執(zhí)行一下useEffect:這和useEffect異步調(diào)度的特點有關(guān),它以一般的優(yōu)先級被調(diào)度,這就意味著一旦有更高優(yōu)先級的任務(wù)進入到commit階段,上一次任務(wù)的useEffect還沒得到執(zhí)行。所以在本次更新開始前,需要先將之前的useEffect都執(zhí)行掉,以保證本次調(diào)度的useEffect都是本次更新產(chǎn)生的。 beforeMutation階段異步調(diào)度useEffect:這個是實打?qū)嵉蒯槍ffectList上有副作用的節(jié)點,去異步調(diào)度useEffect。
function?commitBeforeMutationEffects()?{
??while?(nextEffect?!==?null)?{
????...
????if?((flags?&?Passive)?!==?NoFlags)?{
??????//?如果fiber節(jié)點上的flags存在Passive調(diào)度useEffect
??????if?(!rootDoesHavePassiveEffects)?{
????????rootDoesHavePassiveEffects?=?true;
????????scheduleCallback(NormalSchedulerPriority,?()?=>?{
??????????flushPassiveEffects();
??????????return?null;
????????});
??????}
????}
????nextEffect?=?nextEffect.nextEffect;
??}
}
rootDoesHavePassiveEffects的限制,只會發(fā)起一次useEffect調(diào)度,相當(dāng)于用一把鎖鎖住調(diào)度狀態(tài),避免發(fā)起多次調(diào)度。layout階段填充effect執(zhí)行數(shù)組:真正useEffect執(zhí)行的時候,實際上是先執(zhí)行上一次effect的銷毀,再執(zhí)行本次effect的創(chuàng)建。React用兩個數(shù)組來分別存儲銷毀函數(shù)和 創(chuàng)建函數(shù),這兩個數(shù)組的填充就是在layout階段,到時候循環(huán)釋放執(zhí)行兩個數(shù)組中的函數(shù)即可。
function?commitLifeCycles(
??finishedRoot:?FiberRoot,
??current:?Fiber?|?null,
??finishedWork:?Fiber,
??committedLanes:?Lanes,
):?void?{
??switch?(finishedWork.tag)?{
????case?FunctionComponent:
????case?ForwardRef:
????case?SimpleMemoComponent:
????case?Block:?{
??????...
??????//?layout階段填充effect執(zhí)行數(shù)組
??????schedulePassiveEffects(finishedWork);
??????return;
????}
}
schedulePassiveEffects填充effect執(zhí)行數(shù)組時,有一個重要的地方就是只在包含HasEffect的effectTag的時候,才將effect放到數(shù)組內(nèi),這一點保證了依賴項有變化再去處理effect。也就是:如果前后依賴未變,則effect的tag就賦值為傳入的hookFlags,否則,在tag中加入HookHasEffect標(biāo)志位。正是因為這樣,在處理effect鏈表時才可以只處理依賴變化的effect,use(Layout)Effect才可以根據(jù)它的依賴變化情況來決定是否執(zhí)行回調(diào)。schedulePassiveEffects的實現(xiàn):
function?schedulePassiveEffects(finishedWork:?Fiber)?{
??//?獲取到函數(shù)組件的updateQueue
??const?updateQueue:?FunctionComponentUpdateQueue?|?null?=?(finishedWork.updateQueue:?any);
??//?獲取effect鏈表
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????//?循環(huán)effect鏈表
????do?{
??????const?{next,?tag}?=?effect;
??????if?(
????????(tag?&?HookPassive)?!==?NoHookEffect?&&
????????(tag?&?HookHasEffect)?!==?NoHookEffect
??????)?{
????????//?當(dāng)effect的tag含有HookPassive和HookHasEffect時,向數(shù)組中push?effect
????????enqueuePendingPassiveHookEffectUnmount(finishedWork,?effect);
????????enqueuePendingPassiveHookEffectMount(finishedWork,?effect);
??????}
??????effect?=?next;
????}?while?(effect?!==?firstEffect);
??}
}
enqueuePendingPassiveHookEffectUnmount和enqueuePendingPassiveHookEffectMount填充數(shù)組的時候,還會再異步調(diào)度一次useEffect,但這與beforeMutation的調(diào)度是互斥的,一旦之前調(diào)度過,就不會再調(diào)度了,同樣是rootDoesHavePassiveEffects起的作用。執(zhí)行effect
此時我們已經(jīng)知道,effect得以被處理是因為之前的調(diào)度以及effect數(shù)組的填充。現(xiàn)在到了最后的步驟,執(zhí)行effect的destroy和create。過程就是先循環(huán)待銷毀的effect數(shù)組,再循環(huán)待創(chuàng)建的effect數(shù)組,這一過程發(fā)生在flushPassiveEffectsImpl函數(shù)中。循環(huán)的時候每個兩項去effect是由于奇數(shù)項存儲的是當(dāng)前的fiber。
function?flushPassiveEffectsImpl()?{
??//?先校驗,如果root上沒有?Passive?efectTag的節(jié)點,則直接return
??if?(rootWithPendingPassiveEffects?===?null)?{
????return?false;
??}
??...
??//?執(zhí)行effect的銷毀
??const?unmountEffects?=?pendingPassiveHookEffectsUnmount;
??pendingPassiveHookEffectsUnmount?=?[];
??for?(let?i?=?0;?i?2)?{
????const?effect?=?((unmountEffects[i]:?any):?HookEffect);
????const?fiber?=?((unmountEffects[i?+?1]:?any):?Fiber);
????const?destroy?=?effect.destroy;
????effect.destroy?=?undefined;
????if?(typeof?destroy?===?'function')?{
??????try?{
????????destroy();
??????}?catch?(error)?{
????????captureCommitPhaseError(fiber,?error);
??????}
????}
??}
??//?再執(zhí)行effect的創(chuàng)建
??const?mountEffects?=?pendingPassiveHookEffectsMount;
??pendingPassiveHookEffectsMount?=?[];
??for?(let?i?=?0;?i?2)?{
????const?effect?=?((mountEffects[i]:?any):?HookEffect);
????const?fiber?=?((mountEffects[i?+?1]:?any):?Fiber);
????try?{
??????const?create?=?effect.create;
??????effect.destroy?=?create();
????}?catch?(error)?{
??????captureCommitPhaseError(fiber,?error);
????}
??}
??...
??return?true;
}
useLayoutEffect的同步執(zhí)行
mutation階段執(zhí)行,后者在layout階段執(zhí)行。與useEffect不同的是,它不用數(shù)組去存儲銷毀和創(chuàng)建函數(shù),而是直接操作fiber.updateQueue。//?調(diào)用卸載layout effect的函數(shù),傳入layout有關(guān)的effectTag和說明effect有變化的effectTag:HookLayout | HookHasEffect
commitHookEffectListUnmount(HookLayout?|?HookHasEffect,?finishedWork);
function?commitHookEffectListUnmount(tag:?number,?finishedWork:?Fiber)?{
??//?獲取updateQueue
??const?updateQueue:?FunctionComponentUpdateQueue?|?null?=?(finishedWork.updateQueue:?any);
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??//?循環(huán)updateQueue上的effect鏈表
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????if?((effect.tag?&?tag)?===?tag)?{
????????//?執(zhí)行銷毀
????????const?destroy?=?effect.destroy;
????????effect.destroy?=?undefined;
????????if?(destroy?!==?undefined)?{
??????????destroy();
????????}
??????}
??????effect?=?effect.next;
????}?while?(effect?!==?firstEffect);
??}
}
執(zhí)行本次的effect創(chuàng)建,發(fā)生在layout階段
//?調(diào)用創(chuàng)建layout?effect的函數(shù)
commitHookEffectListMount(HookLayout?|?HookHasEffect,?finishedWork);
function?commitHookEffectListMount(tag:?number,?finishedWork:?Fiber)?{
??const?updateQueue:?FunctionComponentUpdateQueue?|?null?=?(finishedWork.updateQueue:?any);
??const?lastEffect?=?updateQueue?!==?null???updateQueue.lastEffect?:?null;
??if?(lastEffect?!==?null)?{
????const?firstEffect?=?lastEffect.next;
????let?effect?=?firstEffect;
????do?{
??????if?((effect.tag?&?tag)?===?tag)?{
????????//?創(chuàng)建
????????const?create?=?effect.create;
????????effect.destroy?=?create();
??????}
??????effect?=?effect.next;
????}?while?(effect?!==?firstEffect);
??}
}文章轉(zhuǎn)載于 https://www.cnblogs.com/cczlovexw/p/16172130.html
四、總結(jié)
useEffect和useLayoutEffect作為組件的副作用,本質(zhì)上是一樣的。共用一套結(jié)構(gòu)來存儲effect鏈表。整體流程上都是先在render階段,生成effect,并將它們拼接成鏈表,存到fiber.updateQueue上,最終帶到commit階段被處理。
他們彼此的區(qū)別只是最終的執(zhí)行時機不同,一個異步一個同步,這使得useEffect不會阻塞渲染,而useLayoutEffect會阻塞渲染。
五、最后
在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計理念、源碼分層設(shè)計 閱讀下框架官方開發(fā)人員寫的相關(guān)文章 借助框架的調(diào)用棧來進行源碼的閱讀,通過這個執(zhí)行流程,我們就完整的對源碼進行了一個初步的了解 接下來再對源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍
關(guān)注我,一起攜手進階
歡迎關(guān)注前端早茶,與廣東靚仔共同進階~
