Debounce & Throttle in JavaScript

有些事件——比如 scroll 或者 keypress ——发生的频率较高,频繁的触发回调函数可能会造成一些性能上的问题。

比如根据滚动条位置调整某个元素的位置。每次scroll事件的发生都会引起元素位置的变动,不仅不必要,有时还会形成视觉效果上的“抖动”。

解决办法只能是限制回调函数执行的频率。有两种思路

  1. 限定执行函数的间隔事件,即所谓的 "throttle", 限流的意思。
  2. 极端一点,一直等到状态稳定后再执行函数。即 "debounce",去抖动的意思。

setTimeout()

首先是需要可以延后执行代码的功能。在 JavaScript 中,setTimeout() 用于这个功能。

setTimeout(function(){
  console.log("3 seconds later");
}, 3000)

clearTimeout()

clearTimeout() 用于取消先前计划的 setTimeout()

Debounce 去抖动

以监听窗口的 scroll 事件为例,这个事件会频繁地触发。如果每次触发时都需要进行耗时地运算,就会带来性能问题。

但连续地 scroll 事件不会一直发生,会在某个时刻“稳定”下来。这有点像弹簧,在外力作用下会一直晃动,但最终会稳定静止下来。

Debounce 的意思是去抖动,方法是等到状态稳定的时候再进行相应的动作。这需要解决两个问题:

  1. 延迟动作的执行
  2. 判断抖动是否停止

“延迟”可以通过 setTimeout() 做到。判断抖动停止不是很容易做到,

但可以反过来考虑,如果抖动仍然继续,可以取消先前延迟的动作计划,重新计划动作的发生。

window.addEvenetListener('scroll', onScroll);

function onScroll(){
  let timer;

  clearTimeout(timer);

  timer = setTimeout(function(){
     ...
  }, 500);
}

上面的例子是有问题的,timer 这个变量作为局部变量不能在多次调用之间保存状态。

在 JavaScript 里可以通过返回函数的方式绑定一个变量,这个变量存在于返回的函数外部,因而可以在多次调用之间保存状态。

function debounce(callback, delay){
  let timer = null;

  return function(){
    clearTimeout(timer);
    timer = setTimeout(callback, delay);
  }
}

window.addEvenetListener('scroll', debounce(function(){
   ... the task ...
}, 500));

Throttle 限流

在上面 debounce() 的基础上,可以容易得出 throttle() 的实现。

function throttle(callback, delay){
  let timer = 0;

  return function(){
    let now = Date.now();
    if( (now - timer) < delay ) return;
    timer = now;
    setTimeout(callback, delay);
  }
}

资源