JavaScript 系列 - 节流与防抖

100 阅读4分钟

节流

有限时间内执行一次函数

throttle 保证定期执行函数,至少每隔一段时间执行一次,适用于规律性的场景

函数节流.png

实现方式

时间戳

时间戳来判断是否已到执行时间,记录上次执行的时间戳,然后每次触发事件执行回调,回调中判断当前时间戳距离上次执行时间戳的间隔是否已经达到时间差,如果是则执行,并更新上次执行的时间戳,无法做到事件停止触发时无法响应回调

// 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);
    }
  };
};

定时器

使用定时器,比如当事件刚触发时执行回调,然后设置一个定时器,此后每次触发 事件回调,如果已经存在定时器,则回调不执行方法,直到定时器触发,handler 被清除,然后重新设置定时器,无法事件停止触发时必然会响应回调

const throttle = function (func, delay) {
  let timer = null;
  return function () {
    const context = this;
    const args = arguments;
    if (!timer) {
      timer = setTimeout(function () {
        func.apply(context, args);
        timer = null;
      }, delay);
    }
  };
};

结合 debounce 使用

一定时间肯定执行函数

const throttle = function (fn, wait = 100) {
  // preTime 是上一次执行 fn 的时间
  // timer 是定时器
  let preTime = 0,
    timer = null;

  // 将 throttle 处理结果当作函数返回
  return function (...args) {
    // 获取当前时间,转换成时间戳,单位毫秒
    let nowTime = +new Date();

    // ------ 新增部分 start ------
    // 判断上次触发的时间和本次触发的时间差是否小于时间间隔
    if (nowTime - preTime < wait) {
      // 如果小于,则为本次触发操作设立一个新的定时器
      // 定时器时间结束后执行函数 fn
      if (timer) {
        clearTimeout(timer);
        timer = null;
      }
      timer = setTimeout(() => {
        preTime = nowTime;
        fn.apply(this, args);
      }, wait);
      // ------ 新增部分 end ------
    } else {
      // 第一次执行
      // 或者时间间隔超出了设定的时间间隔,执行函数 fn
      preTime = nowTime;
      fn.apply(this, args);
    }
  };
};

使用

  • 游戏中的刷新率
  • DOM 元素拖拽
  • canvas 画笔功能
  • 无限滚动加载更多

防抖

在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时,触发事件经过的时间要比规定的时间长才会触发事件 (乘坐电梯) 有限时间内执行一次函数

函数防抖.png

// WRONG
$(window).on('scroll', function() {
   _.debounce(doSomething, 300); 
});

// RIGHT
$(window).on('scroll', _.debounce(doSomething, 200));

实现方式

定时器

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

function debounce(fn, wait) {
  var timer = null;
  return function (...args) {
    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, wait);
  };
}

实现第一次触发回调事件 (Debounce.Leading)

function debounce(fn, wait, immediate) {
  var timer = null;
  return function (...args) {
    // 如果此时存在定时器的话,则取消之前的定时器重新记时
    if (timer) {
      clearTimeout(timer);
      timer = null;
    }
    if (immediate && !timer) {
      fn.apply(this, args);
    }
    // 设置定时器,使事件间隔指定事件后执行
    timer = setTimeout(() => {
      fn.apply(this, args);
    }, wait);
  };
}

使用

Debounce.Trailing

最后执行函数

Debounce.Leading

立即触发函数

Resize

连续键盘事件

表单提交按钮

节流和防抖区别

requestAnimationFrame (rAF)

window.requestAnimationFrame(callback)  希望执行一个函数,在下次重绘之前再次调用函数。类似 _.throttle(dosomething, 16)

  • callback 参数 DOMHighResTimeStamp 参数,requestAnimationFrame() 开始执行回调函数的时刻

  • window.cancelAnimationFrame(requestID) 取消一个先前通过调用 window.requestAnimationFrame() 方法添加到计划中的动画帧请求

const element = document.getElementById("some-element-you-want-to-animate");
let start, previousTimeStamp;
let done = false;

function step(timestamp) {
  if (start === undefined) {
    start = timestamp;
  }
  const elapsed = timestamp - start;

  if (previousTimeStamp !== timestamp) {
    // 这里使用 Math.min() 确保元素在恰好位于 200px 时停止运动
    const count = Math.min(0.1 * elapsed, 200);
    element.style.transform = `translateX(${count}px)`;
    if (count === 200) done = true;
  }

  if (elapsed < 2000) {
    // 2 秒之后停止动画
    previousTimeStamp = timestamp;
    if (!done) {
      window.requestAnimationFrame(step);
    }
  }
}

window.requestAnimationFrame(step);

优点

  • 内部决定渲染的最佳时间
  • 相当简单和标准的 API

缺点

  • 需要手动管理 rAF 开始/取消
  • 如果选项卡未处于活动状态不会执行
  • node.js 不支持 rAF