JavaScript防抖与节流
概念
防抖(debounce)与节流(throttle)是两个相似但有本质区别的两个概念,但两个概念的存在都是为了控制在特定条件下函数最大的执行次数。这在例如将函数执行onScroll
事件绑定这类事件发生次数过多导致回调函数在任务队列积压、回调函数执行时间过长导致调用栈阻塞容易造成前端性能瓶颈时尤为重要,onScroll
事件在拖动滚动条或在手机页面滑动时会发生30-100次,此时如果回调函数内有略微影响性能的函数执行,这个效果会被放大很多倍。这时防抖与节流处理显得尤为重要。
防抖 Debounce
防抖(Debounce)是将某段时间或某两个特定事件之间的多个调用合为一次调用的操作,如果在规定时间内没有调用,则将前面的多个连续调用合为一个执行。
每个竖线分割的为时间单元,着色为在该时间单元内有事件发生,上方是事件处理前的发生情况,下方是防抖处理后的情况。可以看到防抖处理在事件停止连续执行某一时间段后将发生过的连续多个事件合成为一个事件。
前缘防抖 Leading Edge
前缘防抖(Leading Edge 或 Immediate),是将防抖事件的发生放在事件开始时的一种改良。事件发生时前缘防抖立刻放出一个对应事件,后续连续放生的事件将被前缘防抖过滤,直到事件发生间隔大于设定时间后事件再次发生,节流函数将按照相同方式处理事件。
可以看到前缘防抖会在事件发生时先立刻执行,之后将进行前缘防抖操作。
Lodash的防抖
JavaScript库Lodash包含_.debounce(function, [wait=0], [options={}])
函数可以实现防抖function
为需要防抖的函数,wait
是可选的延迟毫秒数,option.leading
标记是(true
)否(false
)使用前缘防抖这个值默认为false
,option.trailing
标记是否使用默认防抖这个值默认为true
,options.maxWait
标记函数最大延迟时间。
_.debounce(sendEmail, 400, {
leading: true,
trailing: false,
maxWait: 1000
})
实际使用的例子
// 避免窗口在变动时出现昂贵的计算开销。
$(window).on('resize', _.debounce(calculateLayout, 150));
// 当点击时 `sendMail` 随后就被调用。
$(element).on('click', _.debounce(sendMail, 300, {
'leading': true,
'trailing': false
}));
// 确保 `batchLog` 调用1次之后,1秒内会被触发。
var debounced = _.debounce(batchLog, 250, { 'maxWait': 1000 });
var source = new EventSource('/stream');
$(source).on('message', debounced);
// 取消一个 trailing 的防抖动调用
$(window).on('popstate', debounced.cancel);
// 左div会在每次浏览器发出resize时打印,右div会在400ms内无resize事件发出时打印
$(document).ready(function(){
var $win = $(window);
var $left_panel = $('.left-panel');
var $right_panel = $('.right-panel');
function display_info($div) {
$div.append($win.width() + ' x ' + $win.height() + '<br>');
}
$(window).on('resize', function(){
display_info($left_panel);
});
$(window).on('resize', _.debounce(function() {
display_info($right_panel);
}, 400));
});
仅安装Lodash的debounce和throttle功能的方法
npm i -g lodash-cli
lodash include = debounce, throttle
防抖的实现
自行实现防抖与节流其实并不难,只要利用好setTimeout
就可以了
function debounce(method, arguments, ctx, time) {
if (typeof method.tId === "undefined") {
method.tId = 0;
method.call(ctx, ...arguments);
}
if (method.tId) {
clearTimeout(method.tId);
}
method.tId = setTimeout(() => {
method.tId = undefined;
}, time || 500);
}
节流 Throttle
节流(Throttle)的目的是在某段时间内允许某个方法执行一次,这与防抖的间隔指定之间分割连续调用段并在这一段的前或后执行一次方法不同,效果就是节流会保证在某段时间内函数将得到执行机会,而防抖则会将只要是按规定时间连续的不论多久或多少次都会将某方法防抖为一次执行。
简单来说就是,节流是根据距离上次执行被节流函数间隔的时间对调用进行过滤,防抖是根据距离上次调用的时间对调用进行过滤。
节流的实现
首先是前缘节流(不知道有这种说法没)。每一次合理的点击(距离上次执行节流后函数的时间大于time或500ms)都将立即执行,否则不执行
/**
* 将一个函数调用包装为节流调用
* @param {function} method 被包装的方法
* @param {array} arguments 调用方法传入的参数
* @param {object} ctx 上下文
* @param {number} time 节流的限制时间
**/
function throttling(method, arguments, ctx, time) {
if (typeof method.tId === "undefined") { // 节流标记位
method.tId = 0;
method.call(ctx, ...arguments);
method.tId = setTimeout(() => {
method.tId = undefined;
}, time || 500);
}
}
再是后缘节流。第一次点击节流后函数将被立即执行,但后续操作只会每time或500ms之后执行一次,也就是后续操作即使在合理频率下也会有延迟
function throttling(method, arguments, ctx, time) {
if (typeof method.tId === "undefined") { // 节流标记位
method.tId = 0;
method.call(ctx, ...arguments);
return;
}
let tId = method.tId;
if (!tId) {
method.tId = setTimeout(() => {
method.call(ctx, ...arguments);
method.tId = 0;
}, time || 500);
}
}
顺便提一下上述两个函数的使用方法。下方是一个代码片段, 实现当页面按钮点击时在上方获取到的ul
元素上添加一个li
元素,li
元素内部标记有当前addUlElement
函数触发时按钮的点击的次数。可以看到我们使用throttling()
对点击事件进行了包装,当click
事件触发时我们不直接调用addUlElement()
函数本体,而是将调用使用节流方法过滤频率过高的调用,然后以合适的频率执行函数。
function addUlElement(num) {
var li = document.createElement("li");
li.innerHTML = "这是第 " + num + " 次点击按钮产生的li";
ul.appendChild(li);
}
button.addEventListener("click", () => {
throttling(addUlElement, [++num], this, 1000);
});
rAF requestAnimationFrame
实现执行速率限制的另一种方法是使用requestAnimationFrame
,它为60fps的浏览器界面渲染的每帧绑定,它的效果与_.throttle(function, 16)
效果差不多,但是可靠性和性能都更好,因为它是微任务且是浏览器原生API。在滚动、鼠标、键盘事件配合调整元素位置或大小、动画时使用该函数是一个优先选择。
rAF的缺点主要来源于IE9、Opera Mini和一些旧安卓浏览器没有该函数支持,Nodejs也不支持该函数
下面是一个使用rAF的实例,我们使用rAF实现每一帧执行一次动画,如果使用循环则会阻塞且执行结果与速度将不可预期,如果使用setTimeout
或setInterval
又没有办法精准控制执行次数导致动画并不细腻流畅且使用这两个函数执行动画性能较低。
function step(timestamp) {
if (start === undefined)
start = timestamp;
const elapsed = timestamp - start;
//这里使用`Math.min()`确保元素刚好停在200px的位置。
element.style.transform = 'translateX(' + Math.min(0.1 * elapsed, 200) + 'px)';
if (elapsed < 2000) { // 在两秒后停止动画
window.requestAnimationFrame(step);
}
}
window.requestAnimationFrame(step);
总结
- 防抖 debounce:将一段连续多次的调用合成为一个,默认之后执行,前缘防抖将立即执行
- 节流 throttle:保证每段时间内至少执行一次调用,可以用于检查、用户操作等
- rAF:一个节流的更高性能的替代品,最好用于UI相关任务,但不支持IE9