通过例子解释去抖和节流函数
本文翻译自Debouncing and Throttling Explained Through Examples
下面这篇文章是David Corbacho一位伦敦的前端开发工程师写的。我们之前谈到过这个话题,但是这次,David将举出各种生动的例子来让概念更加容易理解。
去抖和节流是两种类似的(但是不同!)的技术,它们用来控制随着时间变化我们允许一个函数被执行的次数。
当我们将一个函数绑定作为DOM事件的处理器时使用去抖或者节流版本的函数尤其管用。为什么?因为这样做我们就给自己在事件与函数执行之间添加了一个控制层。记住,我们不会去控制DOM事件即将会被触发多少次,事件触发的次数是会变化的。
举个例子,让我们来看看滚动事件。看下面的例子:
当我们使用触控板,滚轮或者用鼠标拖动滚动条的时候会轻易地每秒触发30次事件。但是在智能手机上缓慢地滚动页面会很容易的触发100次事件每秒。请问你的事件处理器准备好了这样的触发频率吗?
在2011年,推特网上出现了这样一个问题:当你向下滚动你的简讯页面的时候,页面变得缓慢而且无法响应。John Resig发布了一篇文章a blog post about the problem来描述将做了很多复杂操作的函数绑定到scroll事件上是一个多么坏的主意。
John建议的解决办法(在那个时候,五年前)是在scroll事件之外循环运行函数,每过250ms执行一次。这样的话事件处理函数就不会被同时触发运行。通过这个简单的方法,就可以避免用户执行贵重的操作。
这些天来处理事件的复杂精妙的方法变得更多了。让我来向你介绍去抖,节流和requestAnimationFrame。我们还会看与它们相匹配的使用场景。
Debounce
去抖技术允许我们将多次相继触发的函数调用集合成单一的一次调用。
想象你在一个电梯中。电梯门开始闭合,然后突然间另外一个人想要进入电梯。电梯不会开始执行改变楼层的函数,而是重新打开电梯门。现在另一个人出现了又会重新打开门。电梯虽然会延迟它的函数(变换楼层)执行,但是这样优化了资源。
自己试一试。在按钮上方点击或者移动指针。
从上面的例子可以看到一个单个去抖事件下相继快速发生的事件触发是如何表现的。但是如果事件触发之间有很大的时间间隔,去抖就不会发生。
Leading edge (or "immediate")前边缘(或者说立即)
你也许会觉得恼怒,因为去抖的事件会在触发函数执行之前一直等待,直到事件停止的时候突然触发函数。为什么不立即触发函数执行,这样就会表现地像没有去抖的原始事件处理函数一样?但是不需要在突然执行函数的停顿的时候再次触发函数。
你可以这样做!下面是在等待之前就触发的例子:
在underscore库中,这个配置项叫immediate而不是leading
下面的例子自己试一试:
去抖函数的实现
我第一次看到去抖函数的javascript实现是在2009年在John Hann的这篇博客文章(也是他发明了这个术语)。
之后,Ben Alman创造了一个jquery插件(已经不再维护),一年之后,Jeremy Ashkenas将去抖函数加入到了underscore.js中。最后lodash中也加入了去抖函数。
三种实现的内部代码都有一点区别,但是表面上使用起来都是一致的。
曾有一段时间underscore采用了lodash的去抖和节流实现,在2013年我在_.debounce方法之中发现了一个bug之后。自从那次之后,两个库的实现都变得不一样了。
lodash库为_.debounce和_.throttle函数添加了更多的特性。原来的immediate标识被替换成了leading和trailing选项。你可以选择其中之一或者两个都选。默认情况下只有trailing选项是开启的。
新的maxWait选项(只有lodash库有这个参数)不在本文讨论范围内但是这个参数还是很有用的。实际上,节流函数的定义就是_.debounce和maxWait组合的结果,你可以去看看lodash源码。
去抖函数例子
resize
当改变桌面浏览器窗口的大小的时候,浏览器会触发很多次resize事件当用户拖动窗口大小的时候。
看看下面这个demo:
正如你所见,我们对于resize事件使用默认的trailing配置,因为我们只对于最后的窗口大小值感兴趣,当用户停止改变窗口大小的时候。
鼠标按下事件自动完成表单然后发送ajax请求
当用户仍然在输入的时候,为什么每过50毫秒就要发送请求到服务器?_.debounce可以帮助我们避免额外的工作,只有在用户停止打字的时候才发送请求。
在这里使用leading标识是没有意义的,我们想要等待到用户输入最后一个字母的时候。
一个类似的使用场景就是等待直到用户停止输入然后验证用户的输入,然后提示“密码太短”之类的提示信息。
去抖和节流如何使用?以及容易犯的错误
创建你自己实现的去抖和节流函数是很有吸引力的,或者从网上找一些别人的博文里的实现。我的建议是直接使用underscore和lodash库。如果你只需要库中的_.debounce和_.throttle函数,你可以使用lodash自定义构建器来输出一个自定义的缩小的库。使用下面的命令来创建:
npm i -g lodash-cli
lodash include = debounce, throttle
也就是说,最常见的情况就是使用lodash的throttle和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都可用,也许你会需要这样的功能。
var debounced_version = _.debounce(doSomething, 200); $(window).on('scroll', debounced_version); // If you need it debounced_version.cancel();
Throttle
通过使用_.throttle,我们不允许函数在单位时间X毫秒内执行超过一次。
节流和去抖的主要区别就是节流函数会保证了函数的执行是有规律的,至少每X毫秒只能执行一次。
和去抖函数一样,节流技术可以使用Ben的插件,或者underscore或者lodash。
节流函数例子
无限滚动
这是一个比较普遍的例子。用户向下滚动无限滚动的页面。你需要判断当前滚动的位置和页面底部的距离。如果用户的位置靠近页面底部了,我们应该凭借ajax请求更多的页面内容然后将内容插入页面结尾。
这里_.bounce函数就不能帮上忙了。因为它只能在用户停止滚动的时候触发请求。而我们需要在用户还没有抵达页面底部的时候就开始获取新的内容。
通过_.throttle我们可以保证持续检查当前的位置距离页面底部的距离。
requestAnimationFrame (rAF)
requestAnimationFrame是另外一种方式来限制函数调用的频率。
它可以被认为是_.throttle(dosomething, 16)。但是它拥有更高的保真度,因为它是浏览器原生API以更好的精确性为目标。
我们可以使用rAF API作为一个可选方案来使函数节流,思考下面举出的优缺点:
优点
- 目标为60fps(每16ms一帧)但是内部程序会决定最佳时间如何去安排页面渲染
- 简单而且标准的API,未来不会改变。更少的兼容性问题
缺点
- 什么时候启动和结束rAF方法是我们的责任,而不像debounce或者throttle方法那样在内部被管理。
- 如果浏览器tab页没有被激活,那么它就不会执行。虽然对于滚动,鼠标和键盘事件来说这一点无所谓。
- 虽然所有现代浏览器支持rAF方法,但是IE9,Opera Mini还有旧的安卓手机依然不支持。如今仍然需要一个polyfill。
- nodejs中不支持rAF,所以你无法在服务端使用它来对文件系统的事件函数节流。
单凭经验来说,我会使用requestAnimationFrame方法如果你的js函数是作为绘制页面元素或者动画的用途,如果牵扯了重新计算元素位置那么就应该使用它来控制。
当发送ajax请求,或者决定是否添加或移除一个css class(这样会触发一个css动画),我会考虑使用去抖或者节流方法,这样你可以设置更低的执行比率(200ms执行一次而不是16ms)。
如果你认为rAF方法应该在underscore或lodash库中被实现,但是这两个库都拒绝实现rAF,因为它属于一种特殊使用场景,而且原生的API已经足够简单来直接调用。
rAF例子
我只会在这个例子上演示rAF方法如何控制滚动事件的动画,受到Paul Lewis的文章的启发,他在文章中一步一步解释了这个例子的逻辑。
我将rAF控制的动画和_.throttle节流函数(每16毫秒)控制的动画并排放在一起来直观比较。最后两个函数的表现差不多一样,但是也许rAF方法会在更加复杂的场景中有更好的表现。
Scroll comparison requestAnimationFrame vs throttle
关于rAF技术的更加高级的例子我曾在headroom.js这个库中看到过,其实现的逻辑减弱了而且将操作方法包裹在一个对象中。
Conclusion
使用去抖,节流或者requestAnimationFrame去优化你的事件处理函数。每一种技术都有一点差异,但是这三种方法都很有用并且它们互相补充。
总结:
去抖函数:将突然连续触发的事件(例如频繁按下键盘按键)集合成单独的一次触发。
节流函数:保证每单位时间X毫秒内函数的执行是流畅的。例如每200毫秒就检查一次滚动位置来触发css动画。
requestAnimationFrame函数:一个可选的节流方法。当你的函数在页面上重新计算和渲染元素的时候,或者你想要保证平滑的动画,那么就用它。注意:IE9浏览器不支持这个方法。