防抖和节流通过示例解释

Avatar of David Corbacho
David Corbacho

DigitalOcean 为您旅程的每个阶段提供云产品。 立即开始使用 $200 免费积分!

以下是来自伦敦前端工程师 David Corbacho 的客座文章。 我们之前 探讨过这个主题,但这一次,David 将通过交互式演示来深入讲解这些概念,使它们更加清晰明了。

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

当我们将函数附加到 DOM 事件时,拥有一个防抖或节流版本的函数特别有用。 为什么?因为我们在事件和函数执行之间提供了一层控制。 请记住,我们无法控制这些 DOM 事件的触发频率。 它可能会有所不同。

例如,让我们谈谈滚动事件。 看看这个例子

查看 CodePen 上 Corbacho (@dcorb) 的 滚动事件计数器

使用触控板、滚轮或拖动滚动条进行滚动可以轻松地每秒触发 30 个事件。 但是在我测试中,在智能手机上缓慢滚动(翻页)每秒可以触发高达 100 个事件。 您的滚动处理程序准备好以这种速度执行了吗?

2011 年,Twitter 网站上出现了一个问题:当您向下滚动 Twitter 提要时,它变得缓慢且无响应。 John Resig 发布了 一篇关于这个问题的博文,其中解释了直接将昂贵的函数附加到 scroll 事件是多么糟糕的想法。

John(当时是五年前)建议的解决方案是在 onScroll 事件之外以 250 毫秒的间隔运行一个循环。 这样,处理程序就不会与事件耦合。 通过这种简单的技术,我们可以避免破坏用户体验。

如今,有一些更复杂的方法来处理事件。 让我向您介绍防抖、节流和 requestAnimationFrame。 我们还将了解匹配的用例。

防抖

防抖技术允许我们将多个连续调用“分组”成一个调用。

想象您正在乘坐电梯。 门开始关闭,突然另一个人想上电梯。 电梯不会开始执行其楼层变更功能,门再次打开。 现在,另一个人又这样做了。 电梯正在延迟其功能(移动楼层),但正在优化其资源。

自己试试。 点击或将鼠标移动到按钮上

查看 CodePen 上 Corbacho (@dcorb) 的 防抖。 滞后

您可以看到,连续快速事件是如何由单个防抖事件表示的。 但是,如果事件以较大的间隔触发,则不会发生防抖。

前沿(或“立即”)

您可能会发现防抖事件在等待触发函数执行,直到事件不再发生得如此快,这很烦人。 为什么不立即触发函数执行,使其行为与原始非防抖处理程序完全一样? 但是,在快速调用之间暂停之前不要再次触发。

您可以这样做! 以下是一个在 leading 标志开启时的示例

“前导”防抖的示例。

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

自己试试

查看 CodePen 上 Corbacho (@dcorb) 的 防抖。 前导

防抖实现

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

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

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

有一段时间,underscore 采用了 Lodash 中的防抖/节流实现,这是因为 我在 2013 年发现 _.debounce 函数中存在一个错误。 从那时起,这两种实现就分道扬镳了。

Lodash 已将其 _.debounce_.throttle 函数添加了更多功能。 原始的 immediate 标志被替换为 leadingtrailing 选项。 您可以选择一个或两个。 默认情况下,仅启用 trailing 边缘。

新的 maxWait 选项(目前仅在 Lodash 中)在本篇文章中未涵盖,但它非常有用。 事实上,节流函数是使用 _.debounce 以及 maxWait 定义的,如 lodash 的 源代码 中所示。

防抖示例

调整大小示例

当调整(桌面)浏览器窗口大小时,它们可能会在拖动调整大小句柄时发出许多 resize 事件。

在这个演示中亲眼看看

查看 CodePen 上 Corbacho (@dcorb) 的 防抖调整大小事件示例

如您所见,我们正在使用默认的 trailing 选项用于调整大小事件,因为我们只对用户停止调整大小后的最终值感兴趣。

在使用 Ajax 请求的自动完成表单上按键盘键

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

在这里,拥有 leading 标志是不合理的。 我们想等到最后一个字母被输入。

查看 CodePen 上 Corbacho (@dcorb) 的 防抖按键示例

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

如何使用防抖和节流以及常见陷阱

构建自己的防抖/节流函数,或者从某个随机的博文中复制它,这可能会很诱人。 我的建议是直接使用 underscore 或 Lodash。如果您只需要 _.debounce_.throttle 函数,您可以使用 Lodash 自定义构建器来输出一个自定义的 2KB 压缩库。 使用以下简单的命令构建它

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

也就是说,大多数人使用模块化形式 lodash/throttlelodash/debouncelodash.throttlelodash.debounce 包以及 webpack/browserify/rollup。

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

// WRONG
$(window).on('scroll', function() {
   _.debounce(doSomething, 300); 
});

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

为防抖函数创建一个变量将允许我们调用私有方法 debounced_version.cancel(),该方法在 lodash 和 underscore.js 中可用,以防您需要它。

var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);

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

节流

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

这与防抖的区别在于,节流保证函数定期执行,至少每 X 毫秒执行一次。

与防抖一样,节流技术也由 Ben 的插件 underscore.js 和 lodash 覆盖。

节流示例

无限滚动

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

这里我们心爱的 _.debounce 帮不上忙。它只会在用户停止滚动时触发。而我们需要在用户到达底部之前开始获取内容。
使用 _.throttle,我们可以保证我们一直在检查离底部还有多远。

查看 Pen 无限滚动节流,作者 Corbacho (@dcorb) 在 CodePen 上发布。

requestAnimationFrame (rAF)

requestAnimationFrame 是一种限制函数执行速率的另一种方法。

可以将其视为 _.throttle(dosomething, 16)。但具有更高的保真度,因为它是一个浏览器原生 API,旨在提高准确性。

我们可以将 rAF API 作为节流函数的替代方案,考虑以下优缺点:

优点

  • 旨在实现 60fps(16 毫秒的帧率),但在内部会决定如何安排渲染的最佳时间。
  • 相当简单且标准的 API,未来不会发生变化。维护成本更低。

缺点

  • rAF 的启动/取消由我们负责,不像 .debounce.throttle,它们在内部进行管理。
  • 如果浏览器标签页未处于活动状态,则不会执行。不过对于滚动、鼠标或键盘事件,这无关紧要。
  • 虽然所有现代浏览器都提供 rAF,但 IE9、Opera Mini 和旧版 Android 仍然不支持。需要一个 polyfill 仍然是必需的
  • rAF 在 node.js 中不受支持,因此您不能在服务器上使用它来限制文件系统事件。

一般来说,如果您的 JavaScript 函数直接绘制或动画化属性,请使用 requestAnimationFrame,将其用于所有涉及重新计算元素位置的操作。

要进行 Ajax 请求,或决定添加/删除一个类(这可能会触发 CSS 动画),我会考虑 _.debounce_.throttle,您可以在其中设置更低的执行速率(例如 200 毫秒,而不是 16 毫秒)。

如果您认为 rAF 可以实现到 underscore 或 lodash 中,他们都拒绝了这个想法,因为它是一个专门的用例,并且可以直接调用它很简单。

rAF 示例

我将仅介绍此示例,它是在滚动中使用 requestAnimation 帧,灵感来自 Paul Lewis 的文章,他在文章中逐步解释了此示例的逻辑。

我将它与 16 毫秒的 _.throttle 并排放置,以进行比较。它们提供了类似的性能,但 rAF 可能在更复杂的场景中为您提供更好的结果。

查看 Pen 滚动比较 requestAnimationFrame 与节流,作者 Corbacho (@dcorb) 在 CodePen 上发布。

我在 headroom.js 库中看到了这种技术的更高级示例,其中 逻辑是解耦的,并封装在一个对象中。

结论

使用防抖、节流和 requestAnimationFrame 来优化您的事件处理程序。每种技术都略有不同,但它们都非常有用,并且相互补充。

总结

  • 防抖:将突发事件(如按键)分组为单个事件。
  • 节流:保证每 X 毫秒执行一次,保持持续的执行流程。例如,每 200 毫秒检查一次滚动位置,以触发 CSS 动画。
  • requestAnimationFrame:节流的替代方案。当您的函数重新计算并渲染屏幕上的元素时,并且您希望保证平滑的更改或动画时,请使用它。注意:IE9 不支持。