Description
[译文] 通过例子理解 Throttle(节流) 和 Debounce(去抖)
原文链接:https://css-tricks.com/debouncing-throttling-explained-examples/
引言
Debounce(函数去抖) 和 Throttle(函数节流) 是两个相近但是并非相同的技术,用来控制在一定时间内我们允许执行某个函数的次数。
对于我们的某个函数,对其应用 debounce 或者 throttle 有时候是很有帮助的。比如:当我们将某个函数绑定到 DOM 事件上,为什么呢?因为这样的话我们就可以控制事件和该回调函数的执行了。请记住,我们并不能控制 DOM 事件被触发的频率,这个频率是变化的,我们控制的是回调函数的执行。
比如,我们来看一下滚动事件。请看下面的 Demo:
<div class="demo-1">
<h1>滚动事件的数量</h1>
<a href="#" class="reset">Reset</a>
<div id="counter">0</div>
</div>
import $ from 'jquery';
let i = 0;
const $counter = $('#counter');
$(document).ready(() => {
$(document).on('scroll', () => {
$counter.html(i);
i++;
});
});
$('.reset').on('click', () => {
$counter.html(0);
i = 0;
});
demo 链接: Throttle & Debounce Demo 1
当使用触摸板、鼠标滚轮,甚至拖动滚动条滚动时,可以轻易地触发每秒 30 次滚动事件。更夸张的是,在我的测试中,在智能手机上缓慢滑动都能轻易达到 100 次每秒。你的滚动回调做好处理如此高频执行的准备了吗?
2011 年,Twitter 网站报出一个问题:当你向下拉时间线,滚动会变得越来越慢甚至没有响应。John Resig 发表了一篇博文 John Resig - Learning from Twitter ,阐述了直接添加高性能消耗的滚动事件处理函数是多么严重的做法。
6 年前, John 给出的解决方案是在 onScroll 处理函数之外,运行一个 250ms 的循环,这样事件处理函数就和事件解耦和。用这个简单的做法,他挽救了用户体验。
Debounce
Debounce 技术让我们能够 ”组合“ 多个串行执行调用成为一个调用。
想象一下你身处电梯之中,电梯门开始关闭了,然后一个人忽然想要进来。电梯并没有马上执行变换楼层的”函数“,门再次打开。然后又有一个人想要进来又会发生同样的情况,电梯延缓执行了变换楼层的”函数“,但是充分的利用了它的资源。
下面你可以自己来试一下,鼠标滑到触发区域上并点击按钮:
demo 链接: Throttle & Debounce Demo 2
你可以看到一系列快速触发的事件响应是如何被一个去抖函数代表的。但是如果事件触发的间隔够大,去抖并不会发生。
Leading edge (立即触发)
你也许会觉得一个 Debounce 函数在函数触发之前要”等待“很让人受不了,除非事件停止地很频繁才能频繁触发。所以为什么不立即触发函数执行,让事件的行为更像是一个非 Debounce 函数呢?在频繁的触发中,除非有间歇,才会触发函数执行。
你可以这么做,比如下面的情形就是当我们把 leading
置为 true 的情况:
在 underscore.js
中,这个配置项叫做 immediate
。
线上演示地址:
demo 链接: Throttle & Debounce Demo 3
Debounce 函数的实现
我第一次见到 debounce 函数的 JavaScript 实现是在 2009 年,在这篇文章中 unscriptable.com » Debouncing Javascript Methods。
在那之后很快,Ben Alman 创造了一个 JQuery 插件 现在已经不在维护了。一年后,Jeremy Ashkenas 将 debounce 函数加入到了 underscore.js 中 Initial draft of _.throttle and _.debounce · jashkenas/underscore@9e3e067 · GitHub ,后来又加入到了 lodash 中。
这三种实现方式的内在实现有些区别,但是它们的接口几乎是相同的。
underscore 曾经从 Lodash 借鉴了 debounce/throttle 的实现,直到 2013 年我发现了一个 _.debounce 的 bug。从那以后,两种实现开始各自发展。
Lodash 给它的 _.debounce、 _.throttle 实现加上了更多特性。原始的 immediate
标志被 leading
和 tailing
配置代替。你可以选择一个,或者两者,默认的只有 trailing
被置为 true。
Lodash 的 maxWait 选项(目前只有 Lodash 有这个选项)没有在这片文章中被介绍,但是它是非常有用的功能。事实上,throttle 函数的实现是通过给 _.debounce 函数设置 maxWait 来实现的,你可以在 Lodash 的源代码中看到。
Debounce 实例
window 缩放
当你对桌面浏览器进行缩放的时候,会触发很多的 resize 事件。你可以通过下面的例子来看。
demo 链接:Throttle & Debounce Demo 4
可以看到,我们用了默认的 trailing: true 选项,因为我们在意的是用户听所房后的最终值。
有异步请求的表单上进行输入 keypres
为什么要在用户仍然在输入的情况下每 50ms 就要向服务器发送 Ajax 请求呢?_.debounce 可以帮助我们,不需要多余的工作,并且只要在用户输入完毕后发送一次请求。
这里,我们如果用 leading: true 就太不合理了,我们需要等到用户最终输入完毕。
demo 链接: Throttle & Debounce Demo 5
另一个常见的使用场景是等待用户输入完毕后再提示用户,比如”你输入的密码太短“。
Debounce 和 Throttle 有哪些陷阱?
使用 _.debounce 的一个常见陷阱是多次调用 _.debounce:
比如:
// WRONG
$(window).on('scroll', function() {
_.debounce(doSomething, 300);
});
// RIGHT
$(window).on('scroll', _.debounce(doSomething, 200));
为 Debounce 函数创建一个变量可以允许我们调用取消函数 cancel:
比如:
var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);
// If you need it
debounced_version.cancel();
Throttle
通过使用 _.throttle,我们不允许函数在 X 毫秒内执行超过一次。
Throttle 和 Debounce 的主要区别在于: Throttle 保证至少每 X 毫秒至少执行一次函数。
同样,在 Ben 的插件中、Underscore.js中和 Lodash 中,都包含了 Throttle 方法。
Throttling 实例
无限滚动
一个常见的例子是:用户在一个可以无限滚动的页面上进行向下滚动。你必须检查用户距离页面底部还有多远,如果用户已经将要滚动到底部,我们需要发送 Ajax 请求然后后续内容添加到页面底部。
这时候,我们的 _.debounce 并不能帮上忙,它只是能够保证用户停止滚动后才触发,然后我们在到达底部之前开始请求数据。
使用 _.throttle 我们可以保证始终查询距离底部还有多远。
demo 地址: Throttle & Debounce Demo 6
requestAnimationFrame (rAF)
requestAnimationFrame
是另一种限制函数执行频率的方法,它基本上等同于 _.throttle(doSomething, 16)
,但是有更高的可靠性,因为它是浏览器原生支持的有更高精确度的方法。
我们可以用 rAF 的 API,作为一个可以替代 Throttle 函数的选择,让我们来考虑一下使用 rAF 的利弊:
好处:
- 目标是 60fps(16ms 每帧),但是内部可以决定最好的时机去执行渲染。
- 非常简单并且标准的 API,在未来也不会改变,可以减少维护成本。
坏处:
- rFA 的开始和取消是需要我们去维护的,而
Throttle
和Debounce
则是其内部维护的。 - 如果浏览器没有激活,它就不会执行,无论是对于滚动、点击、或者按键都没有用。
- 虽然大部分现代浏览器都支持,但是仍然有 IE9、Opera Mini、旧的 Android 浏览器仍不支持。需要 polyfill https://www.paulirish.com/2011/requestanimationframe-for-smart-animating/
- rFA 并不支持 node.js,所以你不能在 server 端的 throttle 文件系统操作中使用。
作为一个我非常欣赏的特性,我非常愿意使用 rAF。一般适用于渲染和动画相关的一些方法,比如一些设计重新计算元素位置的操作。
对于发送 Ajax 请求,或者决定删除、添加 class(触发 CSS 动画),我会考虑使用 _.debounce 或者 _.throttle,这样你就能够设定更低的执行频率,比如 200ms 而非 16ms。
如果你觉得 rAF 可以考虑被包含到 underscore 或者 lodash 的实现里,他们都会反对这个观点。因为这是一个比较特殊的使用场景,并且调用起来也足够简单。
requestAnimationFrame 使用场景:
我会使用 rAF 实现这个 scroll 的例子,灵感来自于 Paul Lewis 的文章,他的文章里详细的解释了这个例子。
demo 地址: Throttle & Debounce Demo 7
另一个我见到的使用这种技术的地方是在 headroom.js 库中。
结论
使用 debounce、throttle 和 requestAnimationFrame 来压缩你的事件回调执行。每一种技术都略有不同,但是他们三个都很有用也很完善。
总的来说:
- debounce: 将一系列突然发生的事件(比如按键)组合到一个。
- throttle:保证每 X 毫秒能够持续执行一次,比如每 200ms 检查一下你的滚动位置来触发 CSS 动画。
- requestAnimationFrame:如果你的函数会重新计算或者渲染页面元素,它可以保证平滑的变化和动画。注意:IE9 不支持。