<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    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ù),用位運(yùn)算的方式操作,用來標(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鏈表中,并且單獨(dú)維護(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í),比如看下框架底層是如何運(yùn)行的,以及源碼的閱讀。
        這里廣東靚仔給下一些小建議:
    • 在看源碼前,我們先去官方文檔復(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
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點(diǎn)贊
    評論
    收藏
    分享

    手機(jī)掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    91狠狠色丁香婷婷综合久久 | 国产一级二级视频 | 伊人婷婷丁香 | 精品黄色视频 | 国产成人在线免费视频 |