温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

React之useEffect依赖引用类型问题怎么解决

发布时间:2023-03-14 10:44:41 来源:亿速云 阅读:157 作者:iii 栏目:开发技术

本文小编为大家详细介绍“React之useEffect依赖引用类型问题怎么解决”,内容详细,步骤清晰,细节处理妥当,希望这篇“React之useEffect依赖引用类型问题怎么解决”文章能帮助大家解决疑惑,下面跟着小编的思路慢慢深入,一起来学习新知识吧。

    问题提出

    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>
      );
    };

    在这个案例中,初始化了三个状态,和对应的三个副作用函数useEffect,理想状态是状态的值更新时才触发useEffect。
    多次点击Update Count更新State,因为更新后的值还是1,所以第一个useEffect执行第一次后不会重复执行,这符合预期。但是重复点击Update Person和Update Array时,却不是这样,尽管值相同,但useEffect每一次都会触发。当useEffect中的副作用计算量较大时,必然会引起性能问题。

    原因追溯

    为了追溯这个原因,可以首先熟悉一下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;
    }

    在上面的代码中,我们着重关注areHookInputsEqual的实现,这个函数对比了前后两次传入的依赖项,决定了后续副作用函数create()是否会执行。可以明显看到,useEffect对于依赖项执行的是浅比较,即Object.is (arg1, arg2),这可能是出于性能考虑。对于原始类型这没有问题,但对于引用类型(数组、对象、函数等),这意味着即使内部的值保持不变,引用本身也会发生变化,导致 useEffect执行副作用。

    方案探索

    1.饮鸩止渴

    缝缝补补只是为了等一个人替你推倒重盖

    最直接的思路是把useEffect的依赖项从引用类型换成基本类型:

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

    表面上可行,实际后患无穷(具体参考JSON.stringify为什么不能用来深拷贝),为了避坑而挖另外的坑,显然不是我们期待的解决方案。
    对比之下,这样的写法可以容忍,但是person对象如果增加了其他属性,你要确保自己还记得更新依赖,否则依然是掩盖问题。

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

    2.前置拦截

    第二种思路:

    在你决定要出手之前,我已经帮你决定了 &mdash;&mdash; 格林公式引申公理

    我们可以把问题尽可能前置,手动加一层深对比,如何发现引用值没有变化,就不执行状态更新的逻辑,也就不会触发useEffect重复执行。

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

    但这样显然不太优雅,且每一次写setState时心智负担太重,对比逻辑可不可以封装起来。

    3.他山之石

    实际上自定义的Hooks就是为了解决方法级别的逻辑复用,这里我们利用useRef绑定的值可以跨渲染周期的特点,实现一个自定义的useCompare。

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

    经过ref记录的上一次结果,我们同时拥有了前后两次更新的状态,如果发现值不同,再让ref绑定新的引用类型地址。

    import { isEqual } from 'lodash';
     
    const comparePerson = useCompare(person, isEqual);
     
    useEffect(() => {
        console.log('Component re-rendered by comparePerson');
    }, [comparePerson]);
     
    // 重复执行
    useEffect(() => {
      console.log('Component re-rendered by person');
    }, [person]);

    需要注意的是,这里使用了lodash的isEqual函数实现深对比,看似省心实际是一个成本极其不稳定的选择,如果对象过于庞大,可能得不偿失,可以传入简化的compare函数,有取舍的比较常变的key值。
    而且每次又到单独调用useCompare生成新的对象,这里的逻辑也值得被封装。

    4.回归本质

    停止曲线救国,直面问题本身。

    说了这么多,实际还是useEffect中对比逻辑问题,本着支持拓展但不支持修改的原则,我们需要支持一个新的useEffect支持深度对比。我们将useRef实现的记忆引用传入useEffect的对比逻辑中:

    import { useEffect, useRef } from 'react';
    import isEqual from 'lodash.isequal';
     
    const useDeepCompareEffect = (callback, dependencies, compare) => {
      // 默认的对比函数采用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 发生深层级的改变时才会执行
        console.log('data 发生了改变', data);
      }, [data]);
     
      return <div>Hello World</div>;
    }

    考虑到前文提到的复杂对象的深对比隐患,我依然结和个人意志,在useDeepCompareEffect中加了一个可选参数compare函数,把isEqual作为一种默认模式。

    读到这里,这篇“React之useEffect依赖引用类型问题怎么解决”文章已经介绍完毕,想要掌握这篇文章的知识点还需要大家自己动手实践使用过才能领会,如果想了解更多相关内容的文章,欢迎关注亿速云行业资讯频道。

    向AI问一下细节

    免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

    AI