通过例子解释防抖动和节流

译文说明

作者:David Corbacho
原文链接:https://css-tricks.com/debouncing-throttling-explained-examples/

引言

以下是伦敦前端工程师 David Corbacho 的客座文章。我们已经之前讨论过这个主题,但是这次,David将通过交互式演示来讲解这些概念,使事情变得非常清楚。

Debouncethrottle 是两种类似(但不同的!)的技术,用于控制我们允许一个函数在一段时间内执行多少次。

在将函数附加到DOM事件时,具有函数的防抖动或节流的版本尤其有用。为什么呢?因为我们在事件和函数的执行之间给了自己一个控制层。请记住,我们不控制这些DOM事件的发出频率。它可以变化。

例如,让我们讨论一下滚动事件。看这个例子:


See the Pen
Scroll events counter
by Corbacho (@dcorb)
on CodePen.

当使用触控板、滚动轮或仅仅通过拖动滚动条滚动时,每秒可以轻松触发30个事件。但在我的测试中,在智能手机上缓慢滚动(调换)可能会每秒触发多达100个事件。您的滚动处理程序是否为这种执行速度做好了准备?

2011年,Twitter网站上出现了一个问题:当你向下滚动你的Twitter feed时,它变得缓慢和没有响应。John Resig发表了一篇关于这个问题的文章,文中解释了将开销大的函数直接附加到滚动事件是多么糟糕的想法。

John提出的解决方案(五年前)是在onScroll事件之外,每250ms运行一次循环。这样处理程序就不会耦合到事件。使用这个简单的技术,我们可以避免破坏用户体验。

如今,处理事件的方式稍微复杂了一些。让我来介绍一下Debounce, Throttle, 和requestAnimationFrame。我们还会看到匹配用例。

Debounce

Debounce技术允许我们在一个调用中“分组”多个连续调用。

Example of a debounce

想象你在电梯里。门开始关上,突然另一个人试图上电梯,电梯没有开始它的功能去换楼层,门又开了。现在这种情况再次发生在另一个人身上。电梯推迟了它的功能(移动楼层),但优化了它的资源。

你自己尝试一下。点击或移动按钮上方的鼠标:


See the Pen
Debounce. Trailing
by Corbacho (@dcorb)
on CodePen.

你可以看到连续快速的事件是如何由单个已删除的事件表示的。但如果这些事件是由巨大的差距引发的,那么 debouncing 就不会发生。

Leading edge (or “immediate”)

你可能会发现,在触发函数执行之前,debouncing 事件会一直等待,直到如此快速的事件停止执行。为什么不立即触发函数执行,让它的行为就与原始的非debouncing 处理程序完全相同?除非快速调用暂停,否则不会激发处理函数。

你也可以这样做!下面是一个带着Leading标志的例子:

Example of a “leading” debounce

在 underscore.js 里,该选项被称为 immediate 而不是 leading

自己尝试一下吧:


See the Pen
Debounce. Leading
by Corbacho (@dcorb)
on CodePen.

Debounce Implementations

我第一次看到debounce在JavaScript中实现是在 2009年 John Hann 的文章中(他也创造了这个术语) 。

不久之后,Ben Alman创建了一个jQuery插件(不再维护),一年后,Jeremy Ashkenas将其添加到underscore.js中。后来,它被添加到Lodash中, 完全替代了underscore。

3种实现在内部有点不同,但它们的接口几乎是相同的。

曾经有一段时间,underscore 从Lodash中采用了debounce/throttle实现,在2013年我在_.debounce函数中发现一个错误后,从那时起,这两种实现就分道扬镳了。

Lodash为它的 .debounce.throttle 添加了更多的功能。原来的 immediate 标志被替换为leadingtrailing 选项。你可以选择一个,或者两个都选。默认情况下,只启用了trailing

新的maxWait选项(目前只在Lodash中)不在本文中介绍,但它非常有用。实际上,正如您在lodash源代码中看到的,使用_.debouncemaxWait定义了throttle函数。

Debounce Examples

Resize Example

在调整(桌面)浏览器窗口的大小时,它们可以在拖动“调整大小”操控时,会发出许多 resize 事件。

在这个演示中,自己尝试一下


See the Pen
Debounce Resize Event Example
by Corbacho (@dcorb)
on CodePen.

如你所见,我们使用了resize事件的默认 trailing 选项,因为我们只对最终值感兴趣,即用户停止调整浏览器的大小之后。

在带有Ajax请求的自动完成表单中输入

为什么要在用户仍在输入的情况下,每隔50毫秒向服务器发送一次Ajax请求呢? _.debounce可以帮助我们避免额外的工作,并且只在用户停止输入时发送请求。

在这里,把leading标志位打开是没有意义的。我们要等到最后一个字母打完。


See the Pen
Debouncing keystrokes Example
by Corbacho (@dcorb)
on CodePen.

类似的用例是等待用户停止输入后再验证输入。“您的密码太短”类型的消息。

如何使用 debounce 和throttle,和常见的陷阱

创建自己的debounce/throttle函数,或者从一些随机的博客文章中复制它,这可能很诱人。我的建议是直接使用underscore 或Lodash。如果你只需要 .debounce.throttle函数,您可以使用Lodash自定义生成器来输出一个自定义的2KB缩小库。用这个简单的命令构建它:

1
2
npm i -g lodash-cli
lodash include = debounce, throttle

也就是说,大多数人使用模块形式“lodash/throttle”和“lodash/debounce” 或“lodash”。或者带有“webpack/browserify/rollup” 的 “lodash/throttle” 和 “lodash.debounce” 软件包。

一个常见的陷阱是多次调用_.debounce函数:

1
2
3
4
5
6
7
// WRONG
$(window).on('scroll', function() {
_.debounce(doSomething, 300);
});

// RIGHT
$(window).on('scroll', _.debounce(doSomething, 200));

为debounce函数创建一个变量将允许我们调用私有方法debounced_version.cancel(),如果你需要它的话,该方法可在lodash和underscore.js中获得。

1
2
3
4
5
var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);

// If you need it
debounced_version.cancel();

Throttle

通过使用_.throttle,我们不允许函数每X毫秒执行一次以上。

这与debouncing 的主要区别是,throttle保证定期执行该函数,至少每X毫秒执行一次。

和debounce一样,throttle 技术也被Ben的plugin, underscore.js和lodash所涵盖。

Throttling 示例

无限滚动

一个很常见的例子。用户向下滚动无限滚动页面。您需要检查用户离底部有多远。如果用户接近底部,我们应该通过Ajax请求更多内容并将其附加到页面中。

在这里,我们热衷的 .debounce是没有用的。只有当用户停止滚动时才会触发。我们需要在用户到达底部之前开始获取内容。
.throttle我们可以保证我们不断地检查我们离底部有多远。


See the Pen
Infinite scrolling throttled
by Corbacho (@dcorb)
on CodePen.

requestAnimationFrame (rAF)

requestAnimationFrame是另一种限速(rate-limiting)函数执行的方法。

它可以被认为是一个_.throttle(dosomething, 16)。但它的保真度要高得多,因为它是一个浏览器原生API,目标是更好的精确度。

我们可以使用rAF API,作为throttle 功能的替代,考虑到这些利弊:

利:

  • 目标是60fps(16毫秒的帧),但内部将决定如何安排渲染的最佳时间。
  • 相当简单和标准的API,未来不会改变。更少的维护。

弊:

根据经验,如果您的JavaScript函数是“绘制”或直接动画属性,我将使用requestAnimationFrame,在所有涉及重新计算元素位置的情况下使用它。

要发出Ajax请求,或者决定是否添加/删除一个类(这会触发CSS动画),我将考虑.debounce.throttle,您可以在这里设置更低的执行速率(例如,200ms,而不是16ms)

如果您认为rAF可以在underscore 或lodash中实现,那么它们都拒绝了这个想法,因为它是一个专门的用例,而且很容易直接调用。

Examples of rAF

Paul Lewis文章的启发,我将只讨论这个示例,以便在滚动中使用requestAnimation框架,在这篇文章中,他一步一步地解释了这个示例的逻辑。

在16ms的情况下,我将它和 _.throttle放在一起比较。提供类似的性能,但是rAF可能会在更复杂的场景中提供更好的结果。


See the Pen
Scroll comparison requestAnimationFrame vs throttle
by Corbacho (@dcorb)
on CodePen.

我在headroom.js库中看到过这种技术的更高级的例子。在这里,逻辑被解耦并封装在一个对象中。

总结

使用debouncethrottlerequestAnimationFrame来优化事件处理程序。每种技术略有不同,但这三种技术都是有用的,并且相互补充。

概括的说:

  • debounce: 将突然发生的一系列事件(如击键)组合成一个事件。
  • throttle: 保证每X毫秒执行一次。比如每隔200毫秒检查一次滚动位置,以触发CSS动画。
  • requestAnimationFrame: 一个节流的选择。当你的函数在屏幕上重新计算和渲染元素,你想要保证平滑的变化或动画。注意:不支持IE9。

个人补充:

防抖动(debounce):

所谓的抖动就是浏览器频繁布局时,由于算力不足导致的页面颤动现象。防抖动就是利用类似于节流的手段——无视短时间内重复回调,避免浏览器发生抖动现象的技术。

比较常见的抖动场景是在 auto index 的搜索设计上;当我们在搜索框内输入不同索引时,页面会频繁计算索引并渲染列表,以致产生抖动。但事实上在这类场景里,有价值的请求只会发生在用户停止输入后,通俗来说就是用户输入过程中的字符串不必当真。

节流(throttle):

节流指的都是某个函数在一定时间间隔内只执行第一次回调。

总结:

前端常用的节流和防抖动技术,他们是 JS 闭包和高阶函数的现实应用。

-------------本文结束感谢您的阅读-------------

本文标题:通过例子解释防抖动和节流

文章作者:sanks

发布时间:2020年05月20日 - 23:40

最后更新:2020年07月29日 - 19:46

原始链接:https://www.sanks-blog.com/debounce-and-throttle/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。