【译】Debouncing and Throttling Explained Through Examples
原文地址:
Debouncing and Throttling Explained Through Examples | CSS-Tricks
本文中斜体部分为阅读时的个人理解说明,请注意区分。并且代码中使用的debounce函数和throtte函数均来自于lodash库。
Debounce(bounce意为弹跳,de前缀表示”否定“,所以被称为防抖)和Throttle(作名词讲意为”节流阀“,指控制油料进入发动机量大小的装置)是两种极其相似却又大不相同的技术,用于控制一个函数随着时间的流失执行多少次。
将业务函数的防抖版本或节流版本用在处理DOM事件上是非常有用的。为什么?因为这样可以在事件机制和业务函数执行之间进行控制。毕竟无法那些被动触发的DOM事件的评率不在我们的控制范围内。
举个例子:滚动事件
var i = 0;
var $counter = $('#counter');
$(document).ready(function(){
$(document).on('scroll', function(){
$counter.html(i);
i++;
});
});
当使用触控板,鼠标滚轮或者拖拽滚动条进行滚动操作是,每秒将会触发30个滚动事件。但是在手机上,即使是缓慢的滚动(滑动屏幕),每秒也会触发多达100个事件。你的滚动处理函数能够应对这样的执行速率吗?
2011年,推特的网站上出现一个问题:当你不断滚动信息流,网页会变得越来越慢无法响应。John Resig在文章《a blog post about the problem》中阐释了,将一个执行代价很高的函数直接绑定到scroll事件,是一件多么愚蠢的做法。
在那篇文章中,John建议的解决方案是:在onScroll event
以外使用一个每250ms执行一次的循环。这种方式实现handler和event的解耦合。通过这种简单的技术,可以避免给用户带来的糟糕体验。
最近又出现了一些稍微复杂一点的方式处理事件。让我来介绍一下Debounce,Throttle和rAF。我们还将研究具体的使用场景
Debounce
防抖技术允许我们将多个顺序的调用,聚合到单次调用。
想象你在一个电梯里。电梯门开始关闭,突然另一个人想要进入电梯。电梯不能进行工作去别的楼层,电梯门重新打开。又有另一个人要上电梯,这样的事情还会发生。虽然电梯的行动(上下楼)被突然上人延迟了,但是明显优化了资源利用。
这是一个mousemove
事件的例子:
https://codepen.io/dcorb/pen/KVxGqN
你能看到单个防抖事件,是如何代替快速发生的顺序事件的。如果事件发生的间隔事件,超过防抖设置的延迟,那么就不会进行(也没必要进行)防抖处理。
在进行下面的文章之前,要插入介绍一下lodash中debounce函数:
*_.debounce(func, [wait=0], [options={}])*
options的三个配置说明如下:
- leading:默认false,指定在延迟开始前调用
- maxWait:允许被延迟的最大值。也就是说抖动持续超过这个时间,函数还是要被触发的
- trailing:默认true,指定在延迟结束后调用
Leading属性(或者“立即”)
你会发现令人头疼的一点,在触发函数执行之前,防抖事件总是等待,直到事件不再快速的重复发生。为何不立即触发让函数执行,让它的行为和非防抖handler一样?而是在快速的重复调用过程中不会触发,直到出现一个较长的时间间隔再触发。
你当然可以值么做!设置leading选项:
上面的例子可以理解成防抖处理的函数,在抖动开始时就执行一次,后续的抖动过程中不再执行。没有leading选项(默认trailing选项)时,就相当于抖动结束了后再执行。而抖动结束的判断条件是第二个参数决定的延迟。
https://codepen.io/dcorb/pen/GZWqNV
防抖的实现
我(原文作者)第一次看到JavaScript版本防抖的实现,是在2009年John Hann的博文中(原文已经找不到了),防抖的概念也是从这篇文章开始的。之后很快,Ben Alman开发了一个jQuery插件,再然后,Jeremy Ashkenas将其添加的underscore.js库中。再之后又被加入到lodash库中。这三种实现(jQuery插件,underscore.js和lodash)内部有细微的不同,但是对外暴露的接口几乎一致。
之前有一段时间underscore.js采纳了lodash的防抖/节流实现,但是在2013年,我发现了_.debounce的一个bug(这个bug也无从查证了),从那之后,两个库的实现方式彻底分开。
lodash的debounce函数和throttle函数增加了很多特性,比如leading属性和trailing属性联合取代了immediate属性,默认是trailing模式,但也可以设置成头尾都执行。另外增加了maxWait选项,很有用,实际上节流函数就是借助debounce+maxWait实现的。
源码分析单独写一篇文章,这里不做过多的介绍
防抖的应用场景
resize事件
当调整浏览器窗口尺寸时,会频繁触发resize
事件,用于处理拖动过程。
$(window).on('resize', function(){
display_info($left_panel);
});
$(window).on('resize', _.debounce(function() {
display_info($right_panel);
}, 400));
使用了默认的trailing模式,因为只关心resize的最终结果。从打印结果上不难发现,不使用防抖,会打印出很多拖拽过程中的尺寸信息,假如有一个业务逻辑需要根据尺寸重新布局,或者进行复杂的计算,一方面可能造成卡顿,一方面执行的结果可能只会展示一瞬间, 对用户来说也许并没有意义。
键入信息与异步请求自动补全
用户持续输入时,为何还要每50ms发送一次异步请求?防抖避免了输入过程中不必要的网络工作,当用户结束输入延迟一段时间后(认为是抖动结束)才请求数据。在这个业务中,使用leading模式将导致无法按照预期工作,因为我们关注的是用户输入结束后输入的内容,而不是输入的过程。
如何使用防抖和节流,以及常见错误
作者推荐使用lodash或者underscore.js库提供的相关工具函数,这里略过
常见的错误:重复调用debounce函数。debounce函数用于产生一个防抖版的handler,将这个防抖版的handler绑定到事件处理,而不是在事件处理中调用debounce。
// WRONG
$(window).on('scroll', function() {
_.debounce(doSomething, 300);
});
// RIGHT
$(window).on('scroll', _.debounce(doSomething, 200));
根据lodash的文档说明,debounce创建的函数带有cancel函数,可以用于取消防抖限制:
var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);
// If you need it
debounced_version.cancel();
Throttle
使用节流技术,我们不允许我们的函数每X毫秒执行超过1次。节流和防抖的主要区别,是节流保证了函数是周期执行的。(同样是应对”抖动“场景,防抖意味着不抖动了在执行,忽略所有抖动过程;节流意味着通过自己定义一个频率,让无规律的抖动变成有规律的”抖动“,即可以关注抖动过程中的某些状态,也可以减少多余的工作量)
节流的应用场景
无极滚动
无极滚动是一个很常见的场景。用户在你的页面上会不断的向下滚动。你需要检查用户滚动离底部的距离。如果用户滚动接近了底部,我们就需要请求更多的数据追加到页面上。这个时候防抖显然不那么合适。防抖只会在用户停止滚动后触发一次,但需求是在到达底部之前就要准备好更多数据。使用节流我们可以保证是在持续不断的检查到底部的距离。
// Very simple example.
// Probably you would want to use a
// full-featured plugin like
// https://github.com/infinite-scroll/infinite-scroll/blob/master/jquery.infinitescroll.js
$(document).ready(function(){
// Check every 300ms the scroll position
$(document).on('scroll', _.throttle(function(){
check_if_needs_more_content();
}, 300));
function check_if_needs_more_content() {
pixelsFromWindowBottomToBottom = 0 + $(document).height() - $(window).scrollTop() -$(window).height();
// console.log($(document).height());
// console.log($(window).scrollTop());
// console.log($(window).height());
//console.log(pixelsFromWindowBottomToBottom);
if (pixelsFromWindowBottomToBottom < 200){
// Here it would go an ajax request
$('body').append($('.item').clone());
}
}
requestAnimationFrame (rAF)
requestAnimationFrame
是另一种限制函数执行频率的方式。它可以被认为是_.throttle(dosomething, 16)
。因为它是浏览器为了更好的准确性而提供的原生的API,所以保真度更高。我们可以使用rAF作为节流方式,它具有以下优缺点:
优点
- 针对浏览器60fps渲染频率,每一帧16ms,理想情况下是每一帧执行一次,但实际上浏览器还是会选择合适的时机执行。
- 相当简单且标准的API,未来也不一定会有大的变更,可以减少维护量。
缺点
- rAF的启停都必须手动处理,而防抖和节流都在内部通过定时器进行管理。
- 如果浏览器标签页处于未激活状态,rAF是不会执行的。对于滚动或者鼠标移动等行为,这一点并不影响。
- IE9,Opera Mini和老版本的安卓不提供该API。可能需要polyfill。
- rAF仅限于浏览器环境,node.js无法使用,无法使用rAF来节流服务端文件系统事件。
根据经验,如果你的JavaScript函数是用于绘制或者动画,那么我推荐使用rAF,对于那些包含重新计算元素位置的操作都很适用。而为了进行Ajax请求,或者决定添加/移除样式名(这可能会引起CSS动画),我会考虑使用debounce和throttle,这样你可以设置一个比较低的执行频率。
rAF的应用场景
https://codepen.io/dcorb/pen/pgOKKw
通过一个滚动的例子,对比节流和rAF的效果。由于场景比较简单,所以感觉不出有什么差异,但是在复杂的场景下,可能rAF有更加精确地表现。
总结
使用防抖,节流和rAF都可以优化你的事件处理。每一个技术都有些许差异,但是他们都很有用,并且可以相互补充。
总而言之:
- 防抖:将一系列突发的事件爆发(比如连击按键)聚合成一个单独的事件。
- 节流:保证了一个周期为X毫秒的持续的执行流。比如每200ms检查一次滚动位置。
- rAF:可选的节流方案。当你的函数涉及到重绘和渲染,且想要一定的流畅性或者动画,可以使用rAF。注意不支持IE9。
写在最后:当有人问你一个问题,你回答到一半又问了你一个问题,你停下来回答新的问题,到一半时又被问了第三个问题...每个问题可能都没有答完,这样你整个人的精神状态就可以被称为”抖动“。防抖,也就是拒绝抖动,那我可以在回答问题之前稍微等一下,看你会不会再问下一个问题,要是问了我就干脆不回答了,等到我发现你不会突然打断我了,我再回答问题。这样就不会出现话说一半的尴尬。节流,降低抖动的频率,一个问题都不回答好像也有一些不礼貌,那我就自己心里默念,每过10个数进行一次回答。简单而形象的理解,具体实现可能还有细节,至少这样不会搞得太混。Peace