以下是来自伦敦前端工程师 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
标志被替换为 leading
和 trailing
选项。 您可以选择一个或两个。 默认情况下,仅启用 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/throttle
和 lodash/debounce
或 lodash.throttle
和 lodash.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 不支持。
感谢您撰写了这篇非常详细的文章。这实际上解决了我现在遇到的一个问题。^_^
哇,内容很棒!我一直对防抖、节流和 rAF 3 之间的区别感到困惑,这篇文章让我明白了!
关于 lodash cli 的一个快速问题,它在全局安装后
它会在您的
node_modules
中创建一个仅包含这两个包的 lodash 文件夹吗?它会在当前工作目录中创建一个
lodash.custom.js
和lodash.custom.min.js
。此外,要使用 lodash-cli,您只需调用 lodash。
因此实际上应该是
lodash include=debounce, throttle
应该没有空格,抱歉。
对
throttle
和debounce
的精彩分解!这些视觉效果非常有用。对于大多数视觉任务(如动画和滚动触发器),我倾向于使用
requestAnimationFrame
,就像最后一个示例一样,但我通过使用requestAnimationFrame
返回的正整数(即请求 ID)作为ticking
值来简化操作。如果滚动处理程序被多次调用,则对
ticking
进行简短的检查可以确保requestAnimationFrame
不会再次被调用,直到在update
函数中重置。http://codepen.io/shshaw/pen/PNEwZd?editors=0010
感谢您的解释。对于那些不想安装整个库,只想防抖/节流的人来说,David Walsh 的函数如何适应这一切?
https://davidwalsh.name/javascript-debounce-function
正如 David 在文本中向我们展示的那样,您可以选择仅提取防抖/节流函数。
但就像您一样,我更喜欢使用自己的解决方案:https://gist.github.com/vincentorback/9649034
嗨 Donnie,这篇文章是我写的。
我认为 David Walsh 的函数还可以,但它只是 Underscore.js 多年前实现方式的一个版本,已经过时了。从那时起,已经出现了一些改进。例如,“later”函数不需要在返回函数的作用域内,以及其他改进。
基于这篇博文,Lodash 中有一些优化,而 David Walsh 的版本中没有 http://modernjavascript.blogspot.co.uk/2013/08/building-better-debounce.html
Vincent 的代码片段乍一看似乎不错,我很喜欢它。类似于 Cruz 的“simpler”debounce
https://github.com/rstacruz/simpler-debounce/blob/master/index.js
但 Vincent 的版本由于额外的闭包,支持传递参数。它在 IE9 和更老的 IE 中是否可以运行?(因为它们不支持为 setTimeout 传递多个参数)。此外,它也不支持“leading”选项,这个选项非常方便。
哦,还有,David Walsh 和 Vincent 的 debounce 版本都不支持返回原始函数的值,也不支持 .cancel 方法,……这些是你可能在某些时候需要的额外功能。
我也很想在这篇文章中附带一个“debounce”版本。但我不想鼓励大家复制粘贴不理解的函数。
一般建议,我仍然建议直接使用 Underscore 或 lodash,因为它们经过数百位开发人员的优化和审查,并不断改进,我会信任它们,除非你知道自己在做什么。
http://codepen.io/vlrprbttst/pen/jbKLeo 关于窗口调整大小的去抖动示例!
我真的很感谢这篇文章,它很好地展示了限制事件速率的方法,你的可视化效果很棒!(真的,它们太棒了!)。
不过,去抖动并不是由 John Hann 创造的。它实际上来自硬件开关中使用的类似技术,在按下按钮时,金属触点会相互“弹动”,在电路中触发重复信号。你的键盘上的每个键都有去抖电路(或者如今通常是软件实现),它们基本上做的是同样的事情。
谢谢 NT,我花了几个星期来完善可视化效果。我很高兴人们喜欢它们。
很棒的东西,谢谢!
最后一个例子有点误导。滚动事件和其他与 UI 相关的事件(如 mousemove、resize 等)已经绑定到帧生命周期,现代浏览器(即支持 rAF 的浏览器)不会在一帧内两次调用滚动事件。这意味着,当调用 requestTick 时,ticking 从来不会为 true。只有在你添加第二个事件处理程序(例如“resize”)并调用 requestTick 时,它才有意义。
你可以简单地将你的代码更改为以下代码来验证这一点。
你可以查看这个 codepen 并在调试控制台中查看输出。
好的观点!我没有考虑到这一点。正如我在文章中所说,这个例子是受 Paul Lewis 的文章启发的,我正在将你的评论交叉发布到 http://www.html5rocks.com/en/tutorials/speed/animations/#comment-2663181154,我希望 Paul 能对此有所见解。
我认为你需要更新你的 lodash-cli 命令。我对 lodash 非常陌生,我花了好长时间才发现我必须在命令行中使用 lodash 而不是 lodash-cli。所以对我来说有效的命令是