JS函数节流与防抖

throttle 与 debounce 都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

概念

throttle:连续的时间间隔(每隔一定时间间隔执行callback)。
debounce:空闲的时间间隔(callback执行完,过一定空闲时间间隔再执行callback)。

电梯超时

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。假设电梯有两种运行策略 throttle 和 debounce ,超时设定为15秒,不考虑容量限制。
throttle 策略的电梯。保证如果电梯第一个人进来后,15秒后准时运送一次,不等待。如果没有人,则待机。
debounce 策略的电梯。如果电梯里有人进来,等待15秒。如果又人进来,15秒等待重新计时,直到15秒超时,开始运送。


注意到上面的运行结果,第一行 Mousemove Events 展示了 mousemove 事件触发的频率。第二行和第三行是分别使用 underscore 与 jQuery 的 debounce 方法后事件的触发频率。第四、五行则是增加了一个 delay 参数后的触发频率。与之对比的是最后三行,使用的是 throttle 方法。

debounce的简单封装

在使用中,如果不用到Underscore.js库,那么我们可以自己封装一个throttle与debounce的实现

/**
*
* @param fn {Function} 实际要执行的函数
* @param delay {Number} 延迟时间,也就是阈值,单位是毫秒(ms)
*
* @return {Function} 返回一个“去弹跳”了的函数
*/
function debounce(fn, delay) {

// 定时器,用来 setTimeout
var timer

// 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
return function () {

// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments

// 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
clearTimeout(timer)

// 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),
// 再过 delay 毫秒就执行 fn
timer = setTimeout(function () {
fn.apply(context, args)
}, delay)
}
}

其实思路很简单, debounce 返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数 fn 的执行,强制 fn 只在连续操作停止后只执行一次。

debounce 的使用方式如下:

function ajax_lookup( event ) {
// 对输入的内容$(this).val()执行 Ajax 查询
};

// 字符输入的频率比你预想的要快,Ajax 请求来不及回复。
$('input:text').keyup( ajax_lookup );

// 当用户停顿250毫秒以后才开始查找
$('input:text').keyup( debounce( ajax_lookup. 250 ) );

throttle的简单封装

/**
*
* @param fn {Function} 实际要执行的函数
* @param delay {Number} 执行间隔,单位是毫秒(ms)
*
* @return {Function} 返回一个“节流”函数
*/

function throttle(fn, threshhold) {

// 记录上次执行的时间
var last

// 定时器
var timer

// 默认间隔为 250ms
threshhold || (threshhold = 250)

// 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
return function () {

// 保存函数调用时的上下文和参数,传递给 fn
var context = this
var args = arguments

var now = +new Date()

// 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃
// 执行 fn,并重新计时
if (last && now < last + threshhold) {
clearTimeout(timer)

// 保证在当前时间区间结束后,再执行一次 fn
timer = setTimeout(function () {
last = now
fn.apply(context, args)
}, threshhold)

// 在时间区间的最开始和到达指定间隔的时候执行一次 fn
} else {
last = now
fn.apply(context, args)
}
}
}

原理也不复杂,相比 debounce ,无非是多了一个时间间隔的判断,其他的逻辑基本一致。
throttle 的使用方式如下:

function log( event ) {
console.log( $(window).scrollTop(), event.timeStamp );
};

// 控制台记录窗口滚动事件,触发频率比你想象的要快
$(window).scroll( log );

// 控制台记录窗口滚动事件,每250ms最多触发一次
$(window).scroll( throttle( log, 250 ) );

其它实现

下面是 愚人码头 对throttle与debounce的实现

/*
* 频率控制 返回函数连续调用时,fn 执行频率限定为每多少时间执行一次
* @param fn {function} 需要调用的函数
* @param delay {number} 延迟时间,单位毫秒
* @param immediate {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。
* @return {function}实际调用函数
*/
var throttle = function (fn,delay, immediate, debounce) {
var curr = +new Date(),//当前事件
last_call = 0,
last_exec = 0,
timer = null,
diff, //时间差
context,//上下文
args,
exec = function () {
last_exec = curr;
fn.apply(context, args);
};
return function () {
curr= +new Date();
context = this,
args = arguments,
diff = curr - (debounce ? last_call : last_exec) - delay;
clearTimeout(timer);
if (debounce) {
if (immediate) {
timer = setTimeout(exec, delay);
} else if (diff >= 0) {
exec();
}
} else {
if (diff >= 0) {
exec();
} else if (immediate) {
timer = setTimeout(exec, -diff);
}
}
last_call = curr;
}
};

/*
* 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 delay,fn 才会执行
* @param fn {function} 要调用的函数
* @param delay {number} 空闲时间
* @param immediate {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。
* @return {function}实际调用函数
*/

var debounce = function (fn, delay, immediate) {
return throttle(fn, delay, immediate, true);
};

使用场景

要牵涉到连续事件或频率控制相关的应用都可以考虑到这两个函数,比如:

  • input 中输入文字自动发送 ajax 请求进行自动补全: debounce
  • resize window 重新计算样式或布局:debounce
  • scroll 时更新样式,如随动效果:throttle
    最重要的还是理解两者对调用时间及次数上的处理,根据业务逻辑选择最合适的优化方案!

参考

css-tricks
javascript函数的throttle和debounce