美文网首页让前端飞
手写防抖、节流 hook(ts版)

手写防抖、节流 hook(ts版)

作者: 虚拟J | 来源:发表于2021-05-27 22:02 被阅读0次

    节流与防抖都是通过延迟执行,减少调用次数,来优化频繁调用函数时的性能。不同的是,对于一段时间内的频繁调用,防抖是 延迟执行 一次调用,节流是 延迟定时 多次调用

    前言

    不知道有多少人,简单的写了防抖、节流函数,然后遇到在 react hook 里失效的情况。

    失效的原因: 每次 render 时,内部函数会重新生成并绑定到组件上去。

    解决方案:也很简单,使用 useCallback ,依赖传入空数组,保证 useCallback 永远返回同一个函数。

    上面呢,算是这个文章的一个契机吧。

    关于手写防抖和节流的思路,个人认为关键在于都是对 闭包高阶函数 的应用,以这个为切入点去思考,手写的时候就不会脑子一片空白了。

    防抖(debounce)

    触发事件后在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。

    初步
    import { useCallback } from 'react';
    /**
     * 防抖hook
     * @param func 需要执行的函数
     * @param wait 延迟时间
     */
    export function useDebounce<A extends Array<any>, R = void>(
      func: (..._args: A) => R,
      wait: number,
    ) {
      let timeOut: null | NodeJS.Timeout = null;
      function debounced(..._args: A) {
        if (timeOut) {
          clearTimeout(timeOut);
          timeOut = null;
        }
        timeOut = setTimeout(() => {
          fn.apply(null, _args);
        }, wait);
      }
      return useCallback(debounced, []);
    }
    

    这可以用,但并不够好。想要进阶更高级的工程师,就需要将问题再想深一层,考虑到更复杂的情况,从而自身得到成长。

    进阶版
    1. 首先想到的是要返回一个 Promise ,用来传递返回值。
    2. 其次考虑到异步的情况,增加 async。
    3. 最后是防抖化之后是否可以立即执行和取消,所以增加2个新函数。
    import { useCallback } from 'react';
    /**
     * 防抖hook
     * @param func 需要执行的函数
     * @param wait 延迟时间
     */
    export function useDebounce<A extends Array<any>, R = void>(
      func: (..._args: A) => R,
      wait: number,
    ) {
      let timeOut: null | NodeJS.Timeout = null;
      let args: A;
      function debounce(..._args: A) {
        args = _args;
        if (timeOut) {
          clearTimeout(timeOut);
          timeOut = null;
        }
        return new Promise<R>((resolve, reject) => {
          timeOut = setTimeout(async () => {
            try {
              const result = await func.apply(null, args);
              resolve(result);
            } catch (e) {
              reject(e);
            }
          }, wait);
        });
      }
      //取消
      function cancel() {
        if (!timeOut) return;
        clearTimeout(timeOut);
        timeOut = null;
      }
      //立即执行
      function flush() {
        cancel();
        return func.apply(null, args);
      }
      debounce.flush = flush;
      debounce.cancel = flush;
      return useCallback(debounce, []);
    }
    

    关于防抖函数还有功能更丰富的版本,可以看下 lodashdebounce 函数

    节流(throttle)

    连续触发事件但是在 n 秒中只执行一次函数

    节流函数的2种思路
    • 时间戳:通过记录上次执行的时间戳, 和当前时间戳比较来判断是否已到执行时间 ,如果是则执行,并更新上次执行的时间戳。(问题在于:事件停止触发时无法执行函数)

    • 定时器:如果已经存在定时器,则不执行方法,直到定时器触发后被清除,然后重新设置定时器。(问题在于:事件停止触发后必然会再执行函数)

    整合版

    把两个整合一下,根据场景、需求等来决定,最后是否需要事件停止触发后定时器执行函数。

    /**
     * 节流hook
     * @param func 需要执行的函数
     * @param wait 延迟时间
     * @param isTimer 是否开启定时器响应事件结束后的回调
     */
    export function useThrottle<A extends Array<any>, R = void>(
      func: (..._args: A) => R,
      wait: number,
      isTimer: boolean = false,
    ) {
      let timeOut: null | NodeJS.Timeout = null;
      let args: A;
      let agoTimestamp: number;
      function throttle(..._args: A) {
        args = _args;
        if (!agoTimestamp) agoTimestamp = +new Date();
        if (timeOut) {
          clearTimeout(timeOut);
          timeOut = null;
        }
        return new Promise<R>((resolve, reject) => {
          if (+new Date() - agoTimestamp >= wait) {
            try {
              const result = func.apply(null, args);
              resolve(result);
              agoTimestamp = +new Date();
            } catch (e) {
              reject(e);
            }
          } else if (isTimer) {
            timeOut = setTimeout(async () => {
              try {
                const result = await func.apply(null, args);
                resolve(result);
                agoTimestamp = +new Date();
              } catch (e) {
                reject(e);
              }
            }, agoTimestamp + wait - +new Date());
          }
        });
      }
      //取消
      function cancel() {
        if (!timeOut) return;
        clearTimeout(timeOut);
        timeOut = null;
      }
      //立即执行
      function flush() {
        cancel();
        return func.apply(null, args);
      }
      throttle.flush = flush;
      throttle.cancel = flush;
      return useCallback(throttle, []);
    }
    

    最后

    有个地方有人可能有疑问,为什么没去用 useRef 去保存 timeOut 呢?

    有人可能会认为这会有问题:因为每次组件重新渲染,都会执行一遍所有的 hooks,这样 useDebounce 高阶函数里面的 timeOut 就不能起到缓存的作用(在 useDebounce 里 console.log(timeOut),每次 render 时都打印出 null)。所以 timeOut 不可靠,防抖的核心就被破坏了。

    但是呢,如果你在里面的函数 debounce 里 console.log(timeOut) 会发现,打印出来的,就是之前的 timeOut ,所以是没问题的。

    最后,写的过程中,ts 才是我真正花费时间思考的地方。完成后,有点微妙的满足感。

    相关文章

      网友评论

        本文标题:手写防抖、节流 hook(ts版)

        本文链接:https://www.haomeiwen.com/subject/fmozjltx.html