<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>

          React深入useEffect

          共 11545字,需瀏覽 24分鐘

           ·

          2021-06-01 12:34

          本文適合熟悉React、以及在用useEffect遇到難題的小伙伴進(jìn)行閱讀。

          歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~

          作者:廣東靚仔

          一、前言

          本文基于開源項目:

          https://github.com/facebook/react/blob/master/packages/react/src/ReactHooks.js

              最近有好友問廣東靚仔:怎么寫文章頻率降低了?
              今年廣東靚仔報名了軟考,業(yè)余把精力更多投入到復(fù)習(xí)中。由于疫情影響,今天廣州區(qū)暫停了軟考上半年的相關(guān)科目,廣東靚仔又來寫文章了。
              廣東靚仔將從三個方面來梳理useEffect相關(guān)內(nèi)容:
              1、useEffect介紹
              2、useEffect原理
              3、useEffect源碼解析    
          相信有不少小伙伴在使用useEffect過程中遇到過不少問題,廣東靚仔找來了幾個有bug的例子:
          例子一:
          // 彈框顯示觸發(fā)定時器
          useEffect(() => {
            timer = setInterval(() => {
              if (showModal) {
                requestFun()
              }
            }, 1000)
          }, [showModal])  
          // 關(guān)閉彈框,清除定時器
          const closeModal = () => {
            clearInterval(timer)
          }
          彈框顯示定時器開始執(zhí)行,當(dāng)關(guān)閉彈框。
          定時器居然沒有清除,有bug
          例子二:
          useEffect(() => {
            let intervalId = setInterval(() => {
              fetchData();
            }, 1000 * 60);
            return () => {
              clearInterval(intervalId);
              intervalId = null;
            }
          }, [])
          const fetchData = () => {
             request({params}).then(ret => {
                if (ret.code === OK) {
                    applyResult(ret.data);
                }
             })
          }
          經(jīng)過一番操作后
          引用的值會調(diào)用上一次渲染的值,這是不對的
          例子三:
          當(dāng)我們在useEffect調(diào)用第三方庫的實例,然后在其他函數(shù)清除這個實例,發(fā)現(xiàn)無法清除。
          其他的小伙伴在用useEffect還遇到過其他的問題,這里就不一一展開,閱讀完這篇文章后,一定會對useEffect有一個全面的理解。

          二、useEffect介紹

               React16.8版本中描述了在 React 渲染階段,改變 DOM、添加訂閱、設(shè)置定時器、記錄日志以及執(zhí)行其他包含副作用的操作是不被允許的,因為可能會產(chǎn)生莫名其妙的 bug 并破壞 UI 的一致性。
              因此在使用useEffect完成副作用操作,賦值給useEffect的函數(shù)會在組件渲染到屏幕之后執(zhí)行。useEffect一般是在每輪渲染結(jié)束后執(zhí)行,當(dāng)然我們也可以讓它在只有某些值改變的時候才執(zhí)行。
          useEffect有個清除函數(shù),官方demo如下:
          useEffect(() => {
            const subscription = props.source.subscribe();
            return () => {
              // 清除訂閱
              subscription.unsubscribe();
            };
          });
          一般在執(zhí)行一些計時器或者訂閱,我們會在組件卸載后,會清除這些內(nèi)容。因此可以在清除函數(shù)里面做這些操作。
          useEffect為防止內(nèi)存泄漏,一般情況下如果組件多次渲染,在執(zhí)行下一個effect 之前,上一個 effect 就已被清除。也就是說組件的每一次更新都會創(chuàng)建新的訂閱。
          useEffect 的函數(shù)會在瀏覽器完成布局與繪制之后,在一個延遲事件中被調(diào)用。
          我們都知道一旦 effect 的依賴發(fā)生變化,它就會被重新創(chuàng)建,例如:
          useEffect(
            () => {
              const subscription = props.source.subscribe();
              return () => {
                subscription.unsubscribe();
              };
            },
            [props.source],
          );
           useEffect傳遞第二個參數(shù),它是 effect 所依賴的值數(shù)組。只有當(dāng)依賴改變后才會重新創(chuàng)建訂閱。
          溫馨提示:有很多小伙伴在日常項目開發(fā)的時候,使用這個依賴的時候,很容易留下bug。比如:一個編輯彈框功能,如果useEffect依賴只寫了個id,這個時候如果是對同一條數(shù)據(jù)進(jìn)行編輯是不會再次執(zhí)行useEffect的邏輯的。

          三、useEffect原理

          useEffect實際上是ReactCurrentDispatcher.current.useEffect(源碼解析會講到)

          useEffect原理可以簡單理解為:

          • 函數(shù)組件在掛載階段會執(zhí)行MountEffect,維護(hù)hook的鏈表,同時專門維護(hù)一個effect的鏈表。
          • 在組件更新階段,會執(zhí)行UpdateEffect,判斷deps有沒有更新,如果依賴項更新了,就執(zhí)行useEffect里操作,沒有就給這個effect標(biāo)記一下NoHookEffect,跳過執(zhí)行,去下一個useEffect。


          我們都知道useEffect 在依賴變化時,執(zhí)行回調(diào)函數(shù)。這個變化是指本次 render 和上次 render 時的依賴之間的比較。

          默認(rèn)情況下,effect 會在每輪組件渲染完成后執(zhí)行,而且effect 觸發(fā)后會把清除函數(shù)暫存起來,等下一次 effect 觸發(fā)時執(zhí)行,大概過程如下:


          溫馨提示:使用 hooks 要避免 if、for 等的嵌套使用

          四、useEffrct源碼解析

          在react源碼中,我們找到react.js中如下代碼,篇幅有限,廣東靚仔進(jìn)行了簡化,方便小伙伴閱讀:

          4.1 useEffect引入與導(dǎo)出

          import {
            ...
            useEffect,
            ...
          from './ReactHooks';
          // ReactHooks.js
          export function useEffect(
            create: (
          ) => (() => void) | void,
            depsArray<mixed> | void | null,
          ): void 
          {
            const dispatcher = resolveDispatcher();
            return dispatcher.useEffect(create, deps);
          }
          function resolveDispatcher() {
            const dispatcher = ReactCurrentDispatcher.current;
            if (__DEV__) {
              if (dispatcher === null) {
               // React版本不對或者Hook使用有誤什么的就報錯...
              }
            }

            return ((dispatcher: any): Dispatcher);
          }

          上面的代碼就是引入與導(dǎo)出過程,不難看出useEffect實際上是ReactCurrentDispatcher.current.useEffect橙色的代碼

          import type {Dispatcher} from 'react-reconciler/src/ReactInternalTypes';
          const ReactCurrentDispatcher = {
            current: (nullnull | Dispatcher),
          };
          export default ReactCurrentDispatcher;

          current的類型是null或者Dispatcher,不難看出接下來我們要找類型定義

          // ReactInternalTypes.js
          export type Dispatcher = {|
            useEffect(
              create: () => (() => void) | void,
              depsArray<mixed> | void | null,
            ): void,
          |};

          4.2 組件加載調(diào)用mountEffect

          函數(shù)組件加載時,useEffect會調(diào)用mountEffect,接下來我們來看看mountEffect

          // ReactFiberHooks.new.js
          function mountEffect(
            create: (
          ) => (() => void) | void,
            depsArray<mixed> | void | null,
          ): void 
          {
              return mountEffectImpl(
                PassiveEffect | PassiveStaticEffect,
                HookPassive,
                create,
                deps,
              );
            }

          PassiveEffectPassiveStaticEffect是二進(jìn)制常數(shù),用位運算的方式操作,用來標(biāo)記是什么類型的副作用的。mountEffect走了mountEffectImpl方法

          function mountEffectImpl(fiberFlags, hookFlags, create, deps): void {
            const hook = mountWorkInProgressHook();
            const nextDeps = deps === undefined ? null : deps;
            currentlyRenderingFiber.flags |= fiberFlags;
            hook.memoizedState = pushEffect(
              HookHasEffect | hookFlags,
              create,
              undefined,
              nextDeps,
            );
          }

          上面代碼中,往hook鏈表里追加一個hook,把hook存到鏈表中以后還把pushEffect的返回值存了下來。

          function pushEffect(tag, create, destroy, deps{
            const effect: Effect = {
              tag,
              create,
              destroy, // mountEffectImpl傳過來的是undefined
              deps,
              next: (null: any),
            };
            // 一個全局變量,在renderWithHooks里初始化一下,存儲全局最新的副作用
            let componentUpdateQueue: null | FunctionComponentUpdateQueue = (currentlyRenderingFiber.updateQueue: any);
            if (componentUpdateQueue === null) {
              componentUpdateQueue = createFunctionComponentUpdateQueue();
              currentlyRenderingFiber.updateQueue = (componentUpdateQueue: any);
              componentUpdateQueue.lastEffect = effect.next = effect;
            } else {
              // 維護(hù)了一個副作用的鏈表,還是環(huán)形鏈表
              const lastEffect = componentUpdateQueue.lastEffect;
              if (lastEffect === null) {
                componentUpdateQueue.lastEffect = effect.next = effect;
              } else {
                // 最后一個副作用的next指針指向了自身
                const firstEffect = lastEffect.next;
                lastEffect.next = effect;
                effect.next = firstEffect;
                componentUpdateQueue.lastEffect = effect;
              }
            }
            return effect;
          }

          最后返回了一個effect對象。

          Tips: mountEffect就是把useEffect加入了hook鏈表中,并且單獨維護(hù)了一個useEffect的鏈表。

          4.3 組件更新時調(diào)用updateEffect

          函數(shù)組件加載時,useEffect會調(diào)用updateEffect,接下來我們來看看updateEffect

          function updateEffect(
            create: (
          ) => (() => void) | void,
            depsArray<mixed> | void | null,
          ): void 
          {
            return updateEffectImpl(PassiveEffect, HookPassive, create, deps);
          }
          function updateEffectImpl(fiberFlags, hookFlags, create, deps): void {
            // 獲取當(dāng)前正在工作的hook
            const hook = updateWorkInProgressHook();
            // 最新的依賴項
            const nextDeps = deps === undefined ? null : deps;
            let destroy = undefined;

            if (currentHook !== null) {
              // 上一次的hook的effect
              const prevEffect = currentHook.memoizedState;
              destroy = prevEffect.destroy;
              if (nextDeps !== null) {
                const prevDeps = prevEffect.deps;
                // 比較依賴項是否發(fā)生變化
                if (areHookInputsEqual(nextDeps, prevDeps)) {
                  // 如果兩次依賴項相同,componentUpdateQueue增加一個tag為NoHookEffect = 0 的effect,
                  hook.memoizedState = pushEffect(hookFlags, create, destroy, nextDeps);
                  return;
                }
              }
            }
            // 兩次依賴項不同,componentUpdateQueue上增加一個effect,并且更新當(dāng)前hook的memoizedState值
            currentlyRenderingFiber.flags |= fiberFlags;

            hook.memoizedState = pushEffect(
              HookHasEffect | hookFlags,
              create,
              destroy,
              nextDeps,
            );
          }

          從上面代碼中我們看到areHookInputsEqual用來比較依賴項是否發(fā)生變化。下面我們看看這個areHookInputsEqual函數(shù)

          function areHookInputsEqual(
            nextDeps: Array<mixed>,
            prevDeps: Array<mixed> | null,
          {
           
            if (prevDeps === null) {
              ...
              return false;
            }

            for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
              if (is(nextDeps[i], prevDeps[i])) {
                continue;
              }
              return false;
            }
            return true;
          }

          上面代碼中,廣東靚仔刪掉了一些dev處理的代碼,不影響閱讀。

          其實就是遍歷deps數(shù)組,對每一項執(zhí)行Object.is()方法,判斷兩個值是否為同一個值。

          以上內(nèi)容是源碼中的一部分,如果感興趣的小伙伴可以到react倉庫進(jìn)行閱讀~

          五、總結(jié)

              在我們閱讀完官方文檔后,我們一定會進(jìn)行更深層次的學(xué)習(xí),比如看下框架底層是如何運行的,以及源碼的閱讀。
              這里廣東靚仔給下一些小建議:
          • 在看源碼前,我們先去官方文檔復(fù)習(xí)下框架設(shè)計理念、源碼分層設(shè)計
          • 閱讀下框架官方開發(fā)人員寫的相關(guān)文章
          • 借助框架的調(diào)用棧來進(jìn)行源碼的閱讀,通過這個執(zhí)行流程,我們就完整的對源碼進(jìn)行了一個初步的了解
          • 接下來再對源碼執(zhí)行過程中涉及的所有函數(shù)邏輯梳理一遍

          關(guān)注我,一起攜手進(jìn)階

          如果這篇文章有觸動到你,歡迎關(guān)注前端早茶,與廣東靚仔攜手共同進(jìn)階~


          瀏覽 185
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          評論
          圖片
          表情
          推薦
          點贊
          評論
          收藏
          分享

          手機掃一掃分享

          分享
          舉報
          <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>
                  中国免费毛片视频 | 中国熟妇XXX.1 | 18禁免费网址 | SM欧美手机在线观看 | 国内精品在线看 |