通过例子来解释防抖和节流(译)
前言
通过例子来解释防抖和节流(译)。
原文地址:Debouncing and Throttling Explained Through Examples。
找到这篇文章的契机是看 Lodash 文档的时候看到的,觉得不错,就写写翻译。
正文
本文来自一位伦敦的前端工程师 David Corbacho 。在之前我们已经讨论过这个话题了,但是这次 David 会通过交互的例子来帮助你深入理解这些概念,使其变得简单清晰。
Debounce(防抖)和 Throttle(节流)是两个相似的技术(其实是不同的),他们用来控制一个函数随时间的移动而允许执行的次数。
当我们对一个 DOM 事件绑定一个函数的时候,使用一个具有防抖或者节流功能版本的函数是特别有用的。为什么?因为我们在事件和函数的执行间增加了一个控制层。记住,我们不是控制抛出 DOM 事件的频率,这和前面提到的方式是各不相同的。
举个例子,比如 scroll(滚动)事件,如下:
当通过使用触控板,滚轮或者拖动滚动条来滚动的时候,每秒会触发大约 30 次事件。但在我的测试中,在智能手机中缓慢地滚动会触发多达 100 次事件。你的滚动处理程序能很好地应对这些不同速率的滚动吗?
在 2011 年,Twitter 的 web 站点上出现了一个问题:当在 Twitter 页面内不断向下滚动时,页面会变慢,丢失响应。 John Resig 发布了一篇关于该问题的文章,解释了直接给滚动事件绑定一个昂贵(耗时长)的函数是多么糟糕的一种方式。
John 在当时(5年前,PS:这篇文章在 2016 年发布, 5 年前也就是 2011 年)提出了一个建议的解决方案,通过在 onscroll 事件的外部启动一个 250 毫秒执行一次的定时器。这种方式下事件的处理程序不会和事件耦合。通过这种简单的技术,我们就可以避免影响用户的体验。
如今,处理事件的方式略微复杂了一些。接下来我会介绍 Debounce(防抖)、 Throttle(节流)和 requestAnimationFrame 函数,同时我们会通过相应的用例来说明。
Debounce(防抖)
Debounce(防抖)技术允许我们将多个顺序调用“分组”为一个调用。
想象一下你现在正在电梯内,电梯门开始关闭,突然另一个人想要进来,这时电梯并不会开始楼层的移动,而是打开电梯门,现在,又突然有一个人想要进来,电梯会延后执行楼层移动的过程,虽然楼层移动的过程被延迟了,但却很好地利用了资源。
可以使用下面的例子来测试,在顶部的按钮内点击或者移动鼠标:
你会看到一个防抖事件是如何来表示连续快速的事件的。但如果一个事件触发的间隔很大,那么防抖就不会发生。
leading 前置执行(或者叫立即执行)
防抖事件会在触发函数执行前进行等待,直到事件触发地不是那么快的时候再执行,这种方式可能会让你觉得恼火。为什么不立即触发函数的执行过程,这样不就和原始的,没有防抖的执行过程具有一致的行为?只是在一段快速的触发后不再执行事件。
完全没问题,这是可以实现,下面是一个有着 leading
标志的例子:
在 underscore 库中,该配置的参数名为 immediate
而不是 leading
。
可以自己尝试一下:
Debounce(防抖)的实现
我第一次看到防抖在 JavaScript 中的实现是 2009 年 John Hann 的发布的帖子中(这个“防抖”的术语也是他创造的)。
之后很快, Ben Alman 写了一个 JQuery 的插件(已不再维护),一年之后, Jeremy Ashkenas 把防抖加入到了 underscore 库中。后来 underscore 的替代品 Lodash 中也添加了这个实现。
这三个实现在内部有些许的不同,但他们暴露的接口几乎是完全一样的。
曾经有一段时间, underscore 采用了 Lodash 的 debounce
和 throttle
的实现。2013 年的时候我发现 _.debounce
函数有一个 bug 。至此,两者的实现就开始区分开了。
Lodash 为 _.debounce
和 _.throttle
添加了许多的特性。原始的 immediate
标志替换成了 leading
和 trailing
选项。你可以选择一个或者两个都使用。默认情况下,只会开启后置执行(trailing edge)。
新的 maxWait
选项(目前只有 Lodash 有这个选项)不在本文的提及范内,但是它却是非常有用的。实际上, throttle
(节流)函数是通过使用了 maxWait
参数的 _.debounce
来定义的,你可以在 Lodash 的源码中查看它。
Debounce(防抖)例子
resize 事件的例子
当改变一个浏览器窗口的大小的时候,拖动窗口边缘的缩放句柄会抛出很多的 resize 事件。
可以通过下面的例子来查看:
正如你所看到的,我们为 resize 事件使用了默认的 trailing
参数,因为我们只关心在用户停止缩放窗口后的最后的值。
带有 Ajax 请求的自动完成表单的 keypress 事件的例子
当用户处于键盘键入的状态的时候,为什么要每隔 50ms 去发送一次 Ajax 数据呢? _.debounce
可以帮我避免额外的工作,并且只在用户结束键盘键入之后去发送请求。
在下面的例子中,使用 leading
(前置执行)标志是没有意义的。我们只是希望等待直到键入最后一个字母。
还有一个相似的用例,对于验证用户的输入,我们需要等待直到用户结束键盘键入,然后去展示类似“你的密码太短”之类的消息。
如何使用 debounce 和 throttle 以及常见的陷阱
构建你自己的或者从其他各种博客中复制的 debounce
和 throttle
函数,这种方式看起来非常吸引人。我的推荐是直接使用 underscore 库或者 Lodash 库。如果你只是需要 _.debounce
和 _.throttle
函数,你可以使用 Lodash 自定义的构建流程来输出一个自定义的压缩过后只有 2KB 的库。使用如下的简单命令来构建:
1 | npm i -g lodash-cli |
即大多数时候配合 webpack , browserify , rollup 构建工具来使用诸如 lodash/throttle 和 lodash/debounce 或者 lodash.throttle 和 lodash.debounce 这样模块化的方式。。
调用 _.debounce
函数可能很容易掉入一个常见的陷阱 —— 多次调用:
1 | // 错误 |
用一个变量来存放防抖的函数可以让我们调用其 cancel
方法,在 Lodash 和 underscore 中都包含这个特性,如果你需要的话,可以使用它。
1 | var debounced_version = _.debounce(doSomething, 200); |
Throttle(节流)
通过使用 _.throttle
,我们可以让函数在每 X 毫秒内只执行一次。
节流和防抖的不同之处就是节流保证函数至少在 X 毫秒内执行一次。
和防抖相同, Ben 编写的插件, underscore 库, Lodash 库也实现了这个特性。
Throttle(节流)例子
无限滚动例子
一个相当常见的例子。用户在可以无线滚动的页面上向下滚动。你需要检测用户到底部的距离。如果用户靠近底部了,我们应该通过 Ajax 来请求更多的内容,然后把这些内容更新到页面上。
在这里使用我们“亲爱”的 _.debounce
函数带来的帮助不大。它只会在用户停止滚动之后触发。而我们需要的是在用户到达底部之前就开始获取内容数据。
使用 _.throttle
可以保证我们时刻检测我们离底部的距离。
requestAnimationFrame(rAF)
requestAnimationFrame
是另一种限制函数执行频率的方法。
它可以当作一个 _.throttle(dosomething, 16)
的节流函数,但是由于它是一个浏览器的原生 API ,可用性上会更好。
考虑如下的优点或者缺点,我们可以使用 rAF 作为节流函数的一个可选替代方案。
优点
- 保持 60 帧(即每 16 毫秒 1 帧),但内部会以最好的时机来执行渲染。
- API 相当的简单并且符合标准,在未来基本不改变,这意味着更少的维护。
缺点
- rAF 的启动和取消需要我们手动管理,不像
_.debounce
或者_.throttle
在内部已经处理好了。 - 如果浏览器标签处于非活动状态, rAF 不会执行。尽管对于滚动,鼠标操作或者键盘操作来说,这并不会造成什么问题。
- 尽管所有的现代浏览器都提供了 rAF 函数,但 IE9 ,Opera Mini 和其他老旧的安卓浏览器仍然不支持 rAF ,你可能还是需要一个垫片。
- node 中并不支持 rAF 。所以你无法在服务器中通过它来节流文件系统的事件。
根据我的经验,如果函数跟“绘制”或者改变属性来触发动画的话,我会使用 requestAnimationFrame
,即在所有涉及元素位置重计算的地方使用它。
对于发起 Ajax 请求,是否添加或者删除一个类(可能会触发一个 css 动画)的情况下,我会使用 _.debounce
或者 _.throttle
,相比 rAF ,你可以设置更低的执行频率(比如 200 毫秒,而不是 16 毫秒)。
你可能会想在 underscore 或者 Lodash 中实现 rAF ,不过这两者都拒绝了这个要求,因为这是一个专门的函数,而且很容易调用它。
rAF 例子
对于 rAF ,我只介绍这一个例子,即在滚动中使用 requestAnimationFrame
,这里例子受到 Paul Lewis 文章的启发,在这篇帖子中解释了该例子每一步的逻辑。
我把他和 _.throttle
放一起来作比较。下面的例子中可以得到相似的性能,但某些复杂的场景中 rAF 可能会得到更好的结果。
我在 headroom.js 库中看到关于该技术的一个更高级的例子,其中的逻辑被解耦然后封装到对象内部。
结论
使用 debounce
, throttle
和 requestAnimationFrame
来优化你的事件处理程序。每个技术都有些许的不同,但他们三个都很有作用并且彼此间相辅相成。
总之:
- debounce:将突发的事件(比如键盘点击)分组为一个事件。
- throttle:保证每 X 毫秒内执行一次。比如每 200 毫秒检查滚动的位置,以此来触发一个 css 动画。
- requestAnimationFrame:可选的
throttle
的一个替代。当你的函数重新计算以及重新渲染屏幕上的元素时,你需要保证平滑的变化和动画效果。请注意: IE9 下不支持该 API 。