溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

React之useEffect依賴引用類型問題怎么解決

發(fā)布時(shí)間:2023-03-14 10:44:41 來源:億速云 閱讀:156 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“React之useEffect依賴引用類型問題怎么解決”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“React之useEffect依賴引用類型問題怎么解決”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

    問題提出

    const Issue = function () {
      const [count, setCount] = useState(0);
      const [person, setPerson] = useState({ name: 'Alice', age: 15 });
      const [array, setArray] = useState([1, 2, 3]);
     
      useEffect(() => {
        console.log('Component re-rendered by count');
      }, [count]);
     
      useEffect(() => {
        console.log('Component re-rendered by person');
      }, [person]);
     
      useEffect(() => {
        console.log('Component re-rendered by array');
      }, [array]);
     
      return (
        <div>
          <p>You clicked {count} times</p>
          <button onClick={() => setCount(1)}>Update Count</button>
          <button onClick={() => setPerson({ name: 'Bob', age: 30 })}>Update Person</button>
          <button onClick={() => setArray([1, 2, 3, 4])}>Update Array</button>
        </div>
      );
    };

    在這個(gè)案例中,初始化了三個(gè)狀態(tài),和對應(yīng)的三個(gè)副作用函數(shù)useEffect,理想狀態(tài)是狀態(tài)的值更新時(shí)才觸發(fā)useEffect。
    多次點(diǎn)擊Update Count更新State,因?yàn)楦潞蟮闹颠€是1,所以第一個(gè)useEffect執(zhí)行第一次后不會重復(fù)執(zhí)行,這符合預(yù)期。但是重復(fù)點(diǎn)擊Update Person和Update Array時(shí),卻不是這樣,盡管值相同,但useEffect每一次都會觸發(fā)。當(dāng)useEffect中的副作用計(jì)算量較大時(shí),必然會引起性能問題。

    原因追溯

    為了追溯這個(gè)原因,可以首先熟悉一下useEffect的源碼:

    function useEffect(create, deps) {
      const fiber = get();
      const { alternate } = fiber;
     
      if (alternate !== null) {
        const oldProps = alternate.memoizedProps;
        const [oldDeps, hasSameDeps] = areHookInputsEqual(deps, alternate.memoizedDeps);
     
        if (hasSameDeps) {
          pushEffect(fiber, oldProps, deps);
          return;
        }
      }
     
      const newEffect = create();
     
      pushEffect(fiber, newEffect, deps);
    }
     
    function areHookInputsEqual(nextDeps, prevDeps) {
      if (prevDeps === null) {
        return false;
      }
     
      for (let i = 0; i < prevDeps.length && i < nextDeps.length; i++) {
        if (Object.is(nextDeps[i], prevDeps[i])) {
          continue;
        }
     
        return false;
      }
     
      return true;
    }

    在上面的代碼中,我們著重關(guān)注areHookInputsEqual的實(shí)現(xiàn),這個(gè)函數(shù)對比了前后兩次傳入的依賴項(xiàng),決定了后續(xù)副作用函數(shù)create()是否會執(zhí)行。可以明顯看到,useEffect對于依賴項(xiàng)執(zhí)行的是淺比較,即Object.is (arg1, arg2),這可能是出于性能考慮。對于原始類型這沒有問題,但對于引用類型(數(shù)組、對象、函數(shù)等),這意味著即使內(nèi)部的值保持不變,引用本身也會發(fā)生變化,導(dǎo)致 useEffect執(zhí)行副作用。

    方案探索

    1.飲鴆止渴

    縫縫補(bǔ)補(bǔ)只是為了等一個(gè)人替你推倒重蓋

    最直接的思路是把useEffect的依賴項(xiàng)從引用類型換成基本類型:

      useEffect(() => {
        console.log('Component re-rendered by person');
      }, [JSON.stringify(person)]);
     
      useEffect(() => {
        console.log('Component re-rendered by array');
      }, [JSON.stringify(array)]);

    表面上可行,實(shí)際后患無窮(具體參考JSON.stringify為什么不能用來深拷貝),為了避坑而挖另外的坑,顯然不是我們期待的解決方案。
    對比之下,這樣的寫法可以容忍,但是person對象如果增加了其他屬性,你要確保自己還記得更新依賴,否則依然是掩蓋問題。

    useEffect(() => {
      console.log('Component re-rendered by person');
    }, [person.name, person.age]);

    2.前置攔截

    第二種思路:

    在你決定要出手之前,我已經(jīng)幫你決定了 &mdash;&mdash; 格林公式引申公理

    我們可以把問題盡可能前置,手動加一層深對比,如何發(fā)現(xiàn)引用值沒有變化,就不執(zhí)行狀態(tài)更新的邏輯,也就不會觸發(fā)useEffect重復(fù)執(zhí)行。

    <button onClick={() => {
        const newPerson = { name: 'Bob', age: 18 };
        if (!isEqual(newPerson, person)) {
          setPerson(newPerson)}
        }
      }
    >Update person</button>

    但這樣顯然不太優(yōu)雅,且每一次寫setState時(shí)心智負(fù)擔(dān)太重,對比邏輯可不可以封裝起來。

    3.他山之石

    實(shí)際上自定義的Hooks就是為了解決方法級別的邏輯復(fù)用,這里我們利用useRef綁定的值可以跨渲染周期的特點(diǎn),實(shí)現(xiàn)一個(gè)自定義的useCompare。

    const useCompare = (value, compare) => {
      const ref = useRef(null);
      if (!compare(value, ref.current)) {
        ref.current = value;
      }
      return ref.current;
    }

    經(jīng)過ref記錄的上一次結(jié)果,我們同時(shí)擁有了前后兩次更新的狀態(tài),如果發(fā)現(xiàn)值不同,再讓ref綁定新的引用類型地址。

    import { isEqual } from 'lodash';
     
    const comparePerson = useCompare(person, isEqual);
     
    useEffect(() => {
        console.log('Component re-rendered by comparePerson');
    }, [comparePerson]);
     
    // 重復(fù)執(zhí)行
    useEffect(() => {
      console.log('Component re-rendered by person');
    }, [person]);

    需要注意的是,這里使用了lodash的isEqual函數(shù)實(shí)現(xiàn)深對比,看似省心實(shí)際是一個(gè)成本極其不穩(wěn)定的選擇,如果對象過于龐大,可能得不償失,可以傳入簡化的compare函數(shù),有取舍的比較常變的key值。
    而且每次又到單獨(dú)調(diào)用useCompare生成新的對象,這里的邏輯也值得被封裝。

    4.回歸本質(zhì)

    停止曲線救國,直面問題本身。

    說了這么多,實(shí)際還是useEffect中對比邏輯問題,本著支持拓展但不支持修改的原則,我們需要支持一個(gè)新的useEffect支持深度對比。我們將useRef實(shí)現(xiàn)的記憶引用傳入useEffect的對比邏輯中:

    import { useEffect, useRef } from 'react';
    import isEqual from 'lodash.isequal';
     
    const useDeepCompareEffect = (callback, dependencies, compare) => {
      // 默認(rèn)的對比函數(shù)采用lodash.isEqual, 支持自定義
      if (!compare) compare = isEqual;
      const memoizedDependencies = useRef([]);
      if (!compare (memoizedDependencies.current, dependencies)) {
        memoizedDependencies.current = dependencies;
      }
      useEffect(callback, memoizedDependencies.current);
    };
     
    export default useDeepCompareEffect;
     
     
    function App({ data }) {
      useDeepCompareEffect(() => {
        // 這里的代碼只有在 data 發(fā)生深層級的改變時(shí)才會執(zhí)行
        console.log('data 發(fā)生了改變', data);
      }, [data]);
     
      return <div>Hello World</div>;
    }

    考慮到前文提到的復(fù)雜對象的深對比隱患,我依然結(jié)和個(gè)人意志,在useDeepCompareEffect中加了一個(gè)可選參數(shù)compare函數(shù),把isEqual作為一種默認(rèn)模式。

    讀到這里,這篇“React之useEffect依賴引用類型問題怎么解決”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點(diǎn)還需要大家自己動手實(shí)踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

    向AI問一下細(xì)節(jié)

    免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

    AI