<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          面試官:useEffect和useLayoutEffect有什么區(qū)別?

          共 11427字,需瀏覽 23分鐘

           ·

          2023-10-16 08:46

          您好,如果喜歡我的文章,可以關(guān)注我的公眾號(hào)「量子前端」,將不定期關(guān)注推送前端好文~

          Effect數(shù)據(jù)結(jié)構(gòu)

          顧名思義,React底層在函數(shù)式組件的Fiber節(jié)點(diǎn)設(shè)計(jì)中帶入了hooks鏈表的概念(memorizedState),在此變量上專門存儲(chǔ)每一個(gè)函數(shù)式組件對(duì)應(yīng)的鏈表。

          而對(duì)于副作用(useEffect or useLayoutEffect)來說,對(duì)應(yīng)其hook類型就是Effect

          單個(gè)的effect對(duì)象包括以下幾個(gè)屬性:

          • create: 傳入useEffect or useLayoutEffect函數(shù)的第一個(gè)參數(shù),即回調(diào)函數(shù);

          • destroy: 回調(diào)函數(shù)return的函數(shù),在該effect銷毀的時(shí)候執(zhí)行,渲染階段為undefined

          • deps: 依賴項(xiàng),改變重新執(zhí)行副作用;

          • next: 指向下一個(gè)effect

          • tag: effect的類型,區(qū)分是useEffect還是useLayoutEffect

          單純看這些字段,和平時(shí)使用層面來聯(lián)想還是很通俗易懂的,這里還是補(bǔ)充一下hooks鏈表的概念,有如下的例子:

          const Hello = () => {
              const [ text, setText ] = useState('hello')
              useEffect(() => {
                  console.log('effect1')
                  return () => {
                      console.log('destory1');
                  }
              })
              useLayoutEffect(() => {
                  console.log('effect2')
                  return () => {
                      console.log('destory2');
                  }
              })
              return <div>effect</div>
          }

          掛載到Hello組件fibermemoizedState如下:

          image.png

          可以看到,打印出來結(jié)果和組件中聲明hook的順序是一樣的,不難看出這是一個(gè)鏈表,這也是為什么react hook要求hook的使用不能放在條件分支語句中的原因,如果第一次mount走的是A情況,第二次updateMount走的是B情況,就會(huì)出現(xiàn)hooks鏈表混亂的情況,保證官方范式是比較重要的原因。

          Hook

          從上圖的例子中可以看到,memorizedState的值會(huì)根據(jù)不同hook來決定。

          • 使用useState時(shí),memorizedState對(duì)應(yīng)是string(hello);
          • 使用useEffectuseLayoutEffect,對(duì)應(yīng)的是Effect

          Hook類型如下:

          export type Hook = { 
              memoizedState: any// Hook 自身維護(hù)的狀態(tài) 
              baseQueue: any,
              baseState: any,
              queue: UpdateQueue<anyany> | null// Hook 自身維護(hù)的更新隊(duì)列 
              next: Hook | null// next 指向下一個(gè) Hook 
          };

          創(chuàng)建副作用流程

          基于上面的數(shù)據(jù)結(jié)構(gòu),對(duì)于use(Layout)Effect來說,React做的事情就是

          • render階段:函數(shù)組件開始渲染的時(shí)候,創(chuàng)建出對(duì)應(yīng)的hook鏈表掛載到workInProgressmemoizedState上,并創(chuàng)建effect鏈表,也就是掛載到對(duì)應(yīng)的fiber節(jié)點(diǎn)上,但是基于上次和本次依賴項(xiàng)的比較結(jié)果, 創(chuàng)建的effect是有差異的。這一點(diǎn)暫且可以理解為:依賴項(xiàng)有變化,effect可以被處理,否則不會(huì)被處理。
          • commit階段:異步調(diào)度useEffect或者同步處理useLayoutEffecteffect。等到commit階段完成后,更新應(yīng)用到頁面上之后,開始處理useEffect產(chǎn)生的effect,或是直接處理commit階段同步執(zhí)行阻塞頁面更新的useLayoutEffect產(chǎn)生的effect

          第二點(diǎn)提到了一個(gè)重點(diǎn),就是useEffect和useLayoutEffect的執(zhí)行時(shí)機(jī)不一樣,前者被異步調(diào)度,當(dāng)頁面渲染完成后再去執(zhí)行,不會(huì)阻塞頁面渲染。后者是在commit階段新的DOM準(zhǔn)備完成,但還未渲染到屏幕之前,同步執(zhí)行。

          創(chuàng)建effect鏈表

          useEffect的工作是在currentlyRenderingFiber加載當(dāng)前的hook,具體流程就是判斷當(dāng)前fiber是否已經(jīng)存在hook(就是判斷fiber.memoizedState),存在的話則創(chuàng)建一個(gè)effect hook到鏈表的最后,也就是.next,沒有的話則創(chuàng)建一個(gè)memoizedState

          先看一下創(chuàng)建一個(gè)Effect的入口函數(shù):

          function mountEffect(
              create: () => (() => void) | void,
              deps: Array<mixed> | void | null
          ): void 
          {
              return mountEffectImpl(
                  UpdateEffect | PassiveEffect,
                  HookPassive,
                  create,
                  deps,
              );
          };

          可以看到本質(zhì)上是調(diào)用了mountEffectImpl函數(shù),傳了上一節(jié)所說的Effect type中的字段,這里有個(gè)問題,為什么destroy沒傳呢?獲取上一次effectdestroy函數(shù),也就是useEffect回調(diào)中return的函數(shù),在創(chuàng)建階段是第一次,所以為undefined

          這里看一下創(chuàng)建階段調(diào)用的mountEffectImpl函數(shù):

          function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
            // 創(chuàng)建hook對(duì)象
            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,
            );
          }

          接下來我們都知道,ReactVue都是狀態(tài)改變導(dǎo)致頁面重渲染,而useEffect or useLayoutEffect都會(huì)會(huì)根據(jù)deps變化重新執(zhí)行,所以猜都猜得到,在更新時(shí)調(diào)用的updateEffectImpl函數(shù),對(duì)比mountEffectImpl函數(shù)多出來的一部分內(nèi)容其實(shí)就是對(duì)比上一次的Effect的依賴變化,以及執(zhí)行上一次Effect中的destroy部分內(nèi)容~代碼如下:

          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一個(gè)不帶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,
            );
          }

          可以看到在mountEffectImplupdateEffectImpl中,最后的結(jié)果走向都是pushEffect函數(shù),它的工作很純粹,就是創(chuàng)建出effect對(duì)象,把對(duì)象掛到鏈表中。

          pushEffect代碼如下:

          function pushEffect(tag, create, destroy, deps{
            // 創(chuàng)建effect對(duì)象
            const effect: Effect = {
              tag,
              create,
              destroy,
              deps,
              // Circular
              next: (nullany),
            };

            // 從workInProgress節(jié)點(diǎn)上獲取到updateQueue,為構(gòu)建鏈表做準(zhǔn)備
            let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
            if (componentUpdateQueue === null) {
              // 如果updateQueue為空,把effect放到鏈表中,和它自己形成閉環(huán)
              componentUpdateQueue = createFunctionComponentUpdateQueue();
              // 將updateQueue賦值給WIP節(jié)點(diǎn)的updateQueue,實(shí)現(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í)就是本節(jié)開頭所說的,區(qū)分兩種情況,鏈表為空或鏈表存在的情況,值得一提的是這里的updateQueue是一個(gè)環(huán)形鏈表。

          以上,就是effect鏈表的構(gòu)建過程。我們可以看到,effect對(duì)象創(chuàng)建出來最終會(huì)以兩種形式放到兩個(gè)地方:單個(gè)的effect,放到hook.memorizedState上;環(huán)狀的effect鏈表,放到fiber節(jié)點(diǎn)的updateQueue中。兩者各有用途,前者的effect會(huì)作為上次更新的effect,為本次創(chuàng)建effect對(duì)象提供參照(對(duì)比依賴項(xiàng)數(shù)組),后者的effect鏈表會(huì)作為最終被執(zhí)行的主體,帶到commit階段處理。

          提交階段

          commitRoot

          當(dāng)我們完成更新,進(jìn)入提交重渲染視圖時(shí),主要在commitRoot函數(shù)中執(zhí)行,而在這之前創(chuàng)建Effect以及插入到hooks鏈表中,useEffectuseLayoutEffect其實(shí)做的都是一樣的,也是共用的,在提交階段,我們會(huì)看出兩者執(zhí)行時(shí)機(jī)不同的實(shí)現(xiàn)點(diǎn)。

          // src/react-reconciler/src/ReactFiberWorkLoop.js
          function commitRoot(root{
            // 已經(jīng)完成構(gòu)建的fiber,上面會(huì)包括hook信息
            const { finishedWork } = root;

            // 如果存在useEffect或者useLayoutEffect
            if ((finishedWork.flags & Passive) !== NoFlags) {
              if (!rootDoesHavePassiveEffect) {
                rootDoesHavePassiveEffect = true;
                // 開啟下一個(gè)宏任務(wù)
                requestIdleCallback(flushPassiveEffect);
              }
            }

            console.log('start commit.');
            
            // 判斷自己身上有沒有副作用
            const rootHasEffect = (finishedWork.flags & MutationMask) !== NoFlags;
            // 如果自己的副作用或者子節(jié)點(diǎn)有副作用就進(jìn)行DOM操作
            if (rootHasEffect) {
              // 當(dāng)DOM執(zhí)行變更之后
              console.log('DOM執(zhí)行完畢');
              commitMutationEffectsOnFiber(finishedWork, root);

              // 執(zhí)行l(wèi)ayout Effect
              console.log('開始執(zhí)行l(wèi)ayoutEffect');
              commitLayoutEffects(finishedWork, root);
              if (rootDoesHavePassiveEffect) {
                rootDoesHavePassiveEffect = false;
                rootWithPendingPassiveEffects = root;
              }
            }
            // 等DOM變更之后,更改root中current的指向
            root.current = finishedWork;
          }

          這里的rootDoesHavePassiveEffect是核心判斷點(diǎn),還記得Effect類型中的tag參數(shù)嗎?就是依靠這個(gè)參數(shù)來標(biāo)識(shí)區(qū)分useEffectuseLayoutEffect的。

          rootDoesHavePassiveEffect === false,則執(zhí)行宏任務(wù),將Effect副作用推入宏任務(wù)執(zhí)行棧中。我們可以簡單理解成useEffect的回調(diào)函數(shù)包裝在了requestIdleCallback中去異步執(zhí)行,根據(jù)fiber的知識(shí)接下來會(huì)去走瀏覽器當(dāng)前幀是否有空余時(shí)間來判斷副作用函數(shù)的執(zhí)行時(shí)機(jī)。

          繼續(xù)往下走,如果rootHasEffect === true,代表有副作用,如果是useEffect,副作用已經(jīng)在上面進(jìn)入宏任務(wù)隊(duì)列了,所以如果是useLayoutEffect,就會(huì)在這個(gè)條件中去執(zhí)行,所以在這里我們可以理解到那一句"useEffect和useLayoutEffect的區(qū)別是,前者會(huì)異步執(zhí)行副作用函數(shù)不會(huì)阻塞頁面更新,后者會(huì)立即執(zhí)行副作用函數(shù),會(huì)阻塞頁面更新,不適合寫入復(fù)雜邏輯。"的原因了。

          結(jié)尾

          useEffectuseLayoutEffect十分相似,就連簽名都一樣,不同之處就在于前者會(huì)在瀏覽器繪制后延遲執(zhí)行,而后者會(huì)在所有DOM變更之后同步調(diào)用effect,希望你看到這里,可以對(duì)于這個(gè)結(jié)論的來源有一定的了解和學(xué)習(xí),希望可以幫到你~

          如果喜歡我的文章,可以關(guān)注我的公眾號(hào)「量子前端」,將不定期關(guān)注推送前端好文~


          瀏覽 2126
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  天天操天天草 | 一级黄色视频免费在线观看 | 日韩精品理论 | 特黄毛片 | 日韩吞精|