关于react中防抖节流的实践

1,158 阅读3分钟

关于react中防抖节流的实践

该文仅记录我自己在实践中的一些思考,比较啰嗦,不喜勿喷,欢迎讨论。
可以翻到最后“新情况”,直接食用。

普通的防抖和节流

以下是工具函数的封装(基础版),实现方法很多,这里仅展示作者常用的。

/**
 * 函数节流
 * @param fn 执行函数 需要防抖的函数也就是你处理逻辑的地方
 * @param time 时间间隔
 * @param params 执行函数需要的参数
 * @returns 
 */
export const throttle = (fn, time, params = {}) => {
  let pre = 0;
  return (e) => {
    const now = new Date().getTime();
    if (now - pre >= time) {
      fn({ e, ...params });
      pre = now;
    }
  };
};
/**
 * 函数防抖
 * @param fn 执行函数 需要防抖的函数也就是你处理逻辑的地方
 * @param time 时间间隔
 * @param params 执行函数需要的参数
 * @returns 
 */
export const debounce = (fn, time, params = {}) => {
  let timer;
  return (e) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      fn({ e, ...params });
    }, time);
  };
};

实际项目中的应用(该情况不涉及函数组件的refresh)

节流函数生效

image.png image.png

(setState情况)接着当我们遇到组件refresh的时候,比如在handleClick中去修改一个state

由于触发refresh,每次渲染的时候onClick都对应的是一个新的节流函数
(也就是控制台中每次执行handleClick之后会再一次执行节流函数的初始化)

image.png image.png

接下来你可能会想到用useCallback来对函数进行缓存,尝试一番,如下图还是不行

image.png

再想一想 破案了!!!如下图handleThrottle内的逻辑等同于节流函数内部的逻辑

这样一看清晰很多,原因就在于节流函数内的pre值,每次都是let pre = 0,这时候你又该想到useRef了对吧, 重点就是要保证函数和计时标准(有的是timer,有的是上一次的时间戳)是保持不变的。
考虑到这步,其实就该想到封装成hook工具。

image.png

封装防抖节流hook

/**
 * 函数节流
 * @param fn 执行函数 需要防抖的函数也就是你处理逻辑的地方
 * @param time 时间间隔
 * @param params 执行函数需要的参数
 * @param dep useCallback的依赖项
 * @returns 
 */
export function useThrottle(fn, delay, dep = []) {
  const defaultData: { fn: any; pre: number } = { fn, pre: 0 };
  const { current = { fn: null, pre: 0 } } = useRef(defaultData);
  useEffect(() => {
    current.fn = fn;
  }, [fn]);
  return useCallback((...args) => {
    // 用时间间隔做限制
    const now = new Date().getTime();
    const timeDiff = now - (current?.pre || 0);
    if (timeDiff > delay) {
      current.pre = now;
      current.fn?.(...args);
    }
  }, dep);
}

实际使用

image.png

image.png

!!!新情况 在antd table中添加操作按钮 最好对Button进行封装使用

(如下)在columns中的render调用hooks会有报错,!!!主要意思就是要在合适的位置调用hooks,要满足hooks的规范。

    {
      title: '操作',
      fixed: 'right' as const,
      width: 260,
      render: (_, record) => {
        return (
          <div>
              <Button
                type="link"
                className="primary-color"
                onClick={_utils.useThrottle(handle, 1200, record)}
              >
                配置
              </Button>
          </div>
        );
      },
    },

所以,就考虑把按钮封装成新组件,在每个组件中顶层去调用useThrottle

// 用法
<ThrottleButton
  type="link"
  className="primary-color"
  onClick={() => {
    onConnect(record);
  }}
>
  测试
</ThrottleButton>
// 节流按钮组件
export const ThrottleButton = ({ onClick: fn, ...rest }) => {
  const handle = _utils.useThrottle(fn, 2000);
  return (
    <Button onClick={handle} {...rest}>
      {rest?.children}
    </Button>
  );
};
/**
 * 节流hook  函数组件会经常更新state导致函数重新声明  内部的即使标准也会重置  这里使用useref 和 usecallback进行缓存
 * @param fn 执行函数
 * @param delay 节流周期
 * @param dep 依赖的数据项
 * @returns
 */
export function useThrottle(fn, delay, dep = []) {
  const { current } = useRef({ fn, pre: 0 });
  useEffect(() => {
    current.fn = fn;
  }, [fn]);
  return useCallback((...args) => {
    // 用时间间隔做限制
    const now = new Date().getTime();
    const timeDiff = now - (current.pre || 0);
    if (timeDiff > delay) {
      current.pre = now;
      current.fn?.(...args);
    }
  }, dep);
}

感谢阅读