美文网首页让前端飞
手写防抖、节流 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://gitee.com/r...

  • ts防抖节流

    防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。 使用

  • 手写代码系列(持续更新)

    1、手写instanceOf的实现原理 2、手写节流和防抖函数 2.1 节流函数 节流函数原理:规定在一个单位时间...

  • 前端手写

    节流 防抖 用xhr手写axios 函数柯里化 手写promise 手写reduce new 深拷贝 string...

  • 手写 防抖 节流

    防抖(debounce):一段时间内重复执行的话,只执行最后一次,清除之前的异步任务,重点在清零应用场景: 搜索框...

  • 前端面试中常见手写函数(包括排序算法)

    手写ajax 手写bind函数 手写防抖、节流函数 防抖:简单说就是一开始不会触发,停下来多长时间才会触发,典型案...

  • 手写防抖函数 debounce 和节流函数 throttle

    手写防抖函数 debounce 和节流函数 throttle 本文参考:深入浅出节流函数 throttle深入浅出...

  • JavaScript 基础和面试手写题

    JavaScript 基础和面试手写题 call/bind 的模拟实现 节流/防抖 节流: 当 N 秒内不断触发的...

  • 手写防抖节流函数

    1. 防抖 1.1 什么是防抖? ​ 防抖是触发高频事件后,n秒内函数只会执行一次, 如果n秒内高频事件再次触...

网友评论

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

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