面试官:你是怎么理解防抖与节流的?可以手写吗?

458 阅读4分钟

手写防抖或节流

图片来源网络,仅作配文展示

防抖 (Debounce) 实现

典型例子: 限制 鼠标连击 触发。

一个比较好的解释是:

当一个事件发生后,事件处理器要等一定阈值的时间,如果这段时间过去后,再也没有事情发生,就处理最后一次发生的事情。假设还差 0.01 秒就到达指定之间,这时又来了一个事件,那么之前的等待作废,需要重新再等待指定时间


举一个小例子:假定在做公交车时,司机需等待最后一个人进入后再关门,每次新进一个人,司机就会把计时器清零并重新开始计时,重新等待 1 分钟再关门,如果后续 1 分钟内都没有乘客上车,司机会认为乘客都上来了,将关门发车。 此时「上车的乘客」就是我们频繁操作事件而不断涌入的回调任务;「1 分钟」就是计时器,它是司机决定「关门」的依据,如果有新的「乘客」上车,将清零并重新计时;「关门」就是最后需要执行的函数。

原理及实现

实现原理就是利用定时器,函数第一次执行时设定一个定时器,之后调用时发现已经设定过定时器就清空之前的定时器,并重新设定一个新的定时器,如果存在没有被清空的定时器,当定时器计时结束后触发函数执行。

function debounce(fn, wait = 50, immediate) {
  let timer = null;
  return function(...args) {
    if (timer) clearTimeout(timer);
    // immediate 为 true 表示第一次触发后执行
    // timer 为空表示首次触发
    if (immediate && !timer) {
      fn.apply(this, args);
    }

    timer = setTimeout(() => {
      fn.apply(this, args);
    }, wait);
  };
}
//Demo
// 简单的防抖动函数
// 实际想绑定在 scroll 事件上的 handler
function realFunc() {
  console.log("Success");
}

// 采用了防抖动
window.addEventListener("scroll"debounce(realFunc, 500));
// 没采用防抖动
window.addEventListener("scroll", realFunc);

节流 (Throttle) 实现

可以理解为事件在一个管道中传输,加上这个节流阀以后,事件的流速就会减慢。实际上这个函数的作用就是如此,它可以将一个函数的调用频率限制在一定阈值内,例如 1s,那么 1s 内这个函数一定不会被调用两次。


举一个小例子:不知道大家小时候有没有养过小金鱼啥的,养金鱼肯定少不了接水,刚开始接水时管道中水流很大,水到半满时开始拧紧水龙头,减少水流的速度变成 3 秒一滴,通过滴水给小金鱼增加氧气。 此时「管道中的水」就是我们频繁操作事件而不断涌入的回调任务,它需要接受「水龙头」安排;「水龙头」就是节流阀,控制水的流速,过滤无效的回调任务;「滴水」就是每隔一段时间执行一次函数,「3 秒」就是间隔时间,它是「水龙头」决定「滴水」的依据。

原理及实现

实现方案有两种

  • 第一种是用时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔时候已经到达时间差,如果则执行,并更新上次执行的时间戳,如此循环。
  • 第二种就是使用定时器,比如当 scroll 时间刚触发时,打印一个 hello world,然后设置一个 1000ms 的定时器,此后每次触发 scroll 事件触发回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器。
// fn 是需要执行的函数
// wait 是时间间隔
const throttle = (fn, wait = 50) => {
  // 上一次执行 fn 的时间
  let previous = 0;
  // 将 throttle 处理结果当作函数返回
  return function(...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let now = +new Date();
    // 将当前时间和上一次执行函数的时间进行对比
    // 大于等待时间就把 previous 设置为当前时间并执行函数 fn
    if (now - previous > wait) {
      previous = now;
      fn.apply(this, args);
    }
  };
};
// DEMO
// 执行 throttle 函数返回新函数
const betterFn = throttle(() => console.log("fn 函数执行了"), 1000);
// 每 10 秒执行一次 betterFn 函数,但是只有时间差大于 1000 时才会执行 fn
setInterval(betterFn, 10);