logoahooks dive
Effect

useDeepCompareEffect

用于深度比较依赖的 Hook

用法

用法与 useEffect 一致,不同的是,会深度比较依赖。

effectCount: 0

deepCompareCount: 0

源码

import { useEffect } from 'react';
import { createDeepCompareEffect } from '../createDeepCompareEffect';

export default createDeepCompareEffect(useEffect);
import { useRef } from 'react';
import type { DependencyList, useEffect, useLayoutEffect } from 'react';
import { depsEqual } from '../utils/depsEqual';

type EffectHookType = typeof useEffect | typeof useLayoutEffect;

type CreateUpdateEffect = (hook: EffectHookType) => EffectHookType;

export const createDeepCompareEffect: CreateUpdateEffect = (hook) => (effect, deps) => {
  const ref = useRef<DependencyList>(undefined);
  const signalRef = useRef<number>(0);
  if (deps === undefined || !depsEqual(deps, ref.current)) {
    signalRef.current += 1;
  }
  ref.current = deps;
  hook(effect, [signalRef.current]);
};
import type { DependencyList } from 'react';
import isEqual from 'react-fast-compare';

export const depsEqual = (aDeps: DependencyList = [], bDeps: DependencyList = []) =>
  isEqual(aDeps, bDeps);

解读

首先调用 createDeepCompareEffect 函数,传入 useEffect 函数。

createDeepCompareEffect 执行后返回一个新函数。源码的写法有些简略,因为直接通过箭头函数返回一个新函数,所以就直接省略了 return{}

实际等同于下面的写法:

export const createDeepCompareEffect: CreateUpdateEffect = (hook) => {
  return (effect, deps) => {
    const ref = useRef<DependencyList>(undefined);
    const signalRef = useRef<number>(0);
    if (deps === undefined || !depsEqual(deps, ref.current)) {
      signalRef.current += 1;
    }
    ref.current = deps;
    hook(effect, [signalRef.current]);
  }
};

在解读 createDeepCompareEffect 函数之前,先来思考一个问题:如何实现一个可以自定义比较 depsuseEffecthook

首先肯定还是要在 useEffect 的基础上实现这个 hook。而由于 useEffect 只接受两个参数,第一个参数 effect 是回调函数,第二个参数 deps 是依赖数组。effect 肯定是不能变的,那就只能变 deps 了。那么可以将自定义比较的结果通过 deps 直接传给 useEffect,如果比较的结果为 false 时,那就触发 effect

所以需要一个参数用来传给 useEffect,并且当比较结果为 false 时,这个参数能与上次的值不同。

那么简单点就是将这个参数设置为自增的,每当比较结果为 false 时,就自增 1。同时用 useRef 记录每次比较的结果。

而每次传入的 deps 同样也需要用 useRef 记录,用于每次比较时,判断 deps 是否发生变化。

所以,最后实现的代码如下:

import { useEffect, useRef } from 'react';

export const useCustomCompareEffect = (effect, deps, isEqual = Object.is) => {
  const depsRef = useRef(undefined);
  const signalRef = useRef(0);
  if (deps === undefined || !isEqual(deps, depsRef.current)) {
    signalRef.current += 1;
  }
  depsRef.current = deps;
  useEffect(effect, [signalRef.current]);
};

useCustomCompareEffect 函数接收三个参数:effect 是回调函数,deps 是依赖数组,isEqual 是自定义比较函数。

如果 isEqual 未传入,则默认使用 Object.is 进行比较。并且当前的执行逻辑与 useEffect 一致。

函数内部将 depsRef 的初始值设置为 undefined,因为 useEffectdeps 是可选的。如果未传入,则需要每次渲染时都触发 effect

再将 signalRef 的初始值设置为 0,用于后续自增。

接着判断 deps 是否为 undefined,或者是否与上次记录的 deps 不一致,如果不一致,则自增 signalRef

然后更新 depsRef 的值为当前最新的 deps

最后将 signalRef 的值作为 deps 传给 useEffect,用于触发 effect

小测一下~

effectCount: 0

customCompareCount: 0

了解了 useCustomCompareEffect 的实现后,这时候再看 createDeepCompareEffect 的实现,一切都豁然开朗了。

无非就是在 createDeepCompareEffect 内部默认用 react-fast-compareisEqual 来比较 deps 是否发生变化。其余部分与 useCustomCompareEffect 一致。

export const createDeepCompareEffect: CreateUpdateEffect = (hook) => {
  return (effect, deps) => {
    const ref = useRef<DependencyList>(undefined);
    const signalRef = useRef<number>(0);
    if (deps === undefined || !depsEqual(deps, ref.current)) { 
      signalRef.current += 1;
    }
    ref.current = deps;
    hook(effect, [signalRef.current]);
  }
};

关于 react-fast-compare 的性能,与其他常用库对比了一下:

benchmark

在对比原始值时,react-fast-compare 的性能居末,但对比对象、数组时,性能位居前二。尤其是对象的比对,测了好几轮,一直是第一。可见其在 React 中用来对比 deps,还是很能打的。

Last updated on

On this page