Debouncing和Throttling
次访问
原文地址Debounce
和 throttle
是两种相似但不同的技术,用来控制函数在一定时间内执行的次数,简单说是用来限频。
当我们的函数操作DOM
事件时,对函数用使用Debounce
或 throttle
非常有用,因为我们在DOM
事件和函数执行之间加了我们的控制层。
当我们通过触控板、滚轮、拖动滚动条来滚动时,会很轻易的每秒触发30次滚动事件,但是在智能手机上测试缓慢的滚动时,每秒可触发多达100次滚动事件,你的滚动回调为这样高频的执行做好准备了吗?
2011年,Twitter网站突现了一个问题:页面在向下滚动时变得卡顿。John Resig发表了一篇关于这个问题的博文,来解释直接将昂贵的函数绑定到滚动事件上多么糟糕。John建议在滚动回调函数外包裹一个每250ms执行一次的循环,这样滚动回调不与滚动事件耦合,简单的避免了用户体验差的问题。
现在处理类似的高频事件的方式稍微复杂点。下面介绍Debounce
, Throttle
, 和 requestAnimationFrame
。
Debounce
Debounce
可以把连续的多个调用分组到一个调用中。
想象下你在电梯里,门开始关闭,突然有人要进来,电梯没有改变楼层,门又开了,每次要关门时,如果有人要进来,都会再次开门,电梯在推迟移动到其他楼层,但在优化资源。
Leading edge(或immediate)
在事件发生间隔变的足够长之前,Debounce会一直等待,推迟回调的执行,为什么不立刻触发回调的执行,看起来就像没有用Debounce处理过,只是在快速连续的触发事件停止之前不要再次执行,就像下面这样:
在underscore.js中, 这个参数叫immediate,而不是leading。
Debounce实现
第一次看到Debounce的js实现是2009年John Hann的博文《Debouncing Javascript Methods》,此后不久,Ben Alman创建了一个jQuery插件(不再维护),一年后,Jeremy Ashkenas将其添加到underscore.js中。后来,它被添加到Lodash中。这3种实现在内部略有不同,但它们的接口几乎相同。有一段时间underscore.js采用了Lodash的debounce/throttle实现,直到我2013年发现了_.debounce
函数的一个bug,这两种实现逐渐不同。
Lodash在_.debounce
和_.throttle
中添加了更多的功能。原来的immediate
参数被leading
和trailing
替代,你可以选择1个,或两个都选,默认只用trailing
。
本文没有介绍的maxWait
参数(目前仅在Lodash
中)是非常有用的,实际上,在lodash
源码中可以看到,Throttle函数的定义_.debounce
中有maxWait
。
Debounce示例
1、resize
在拖动大小控制器来resize
浏览器窗口时,可以触发很多次resize
事件。
完整示例请查看原文Debouncing and Throttling Explained Through Examples | CSS-Tricks
1 | // Based on http://www.paulirish.com/2009/throttled-smartresize-jquery-event-handler/ |
resize
事件中我们用了默认参数trailing
,因为我们只关心浏览器停止改变窗口大小后的最终值。
2、按键输入关联的ajax请求
为什么我们要在用户还是输入的时候每50ms向服务器发ajax请求呢?_.debounce
可以帮助我们避免这种冗余的造作,只在用户停止输入的时候发送请求。
此时,使用leading
参数没有意思,我们要等最后一个字符输入完毕。
1 | $(document).ready(function(){ |
类似的情形还可能是等到用户停止输入再校验其输入内容,然后显示类似“您的密码位数太短”的提示语。
如何使用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
的包。
一个常见的坑是不止1次的调用_.debounce
函数:
1 | // 错误的写法 |
如果你有需要,在Lodash
和underscore.js
中,为需要debounce
的函数创建一个变量将允许我们调用私有方法debounced_version.cancel()
。
1 | var debounced_version = _.debounce(doSomething, 200); |
Throttle
使用了_.throttle,我们不允许函数每X毫秒执行超过1次。
throttle
和debounce
的主要区别是,throttle
保证函数有规律的执行,至少每X毫秒执行1次。
和debounce
一样,throttle
也包含在Ben的插件、underscore.js
和lodash
中。
Throttle示例
1、无限滚动
一个常见的例子:用户向下滑动你的无限滚动页面,你需要检查用户距离页面底部有多远,如果快滑到底部了,你就要通过ajax
请求更多的数据来填充页面。
此时_.debounce
没用了,它只能在用户停止滑动时触发,而我们需要在用户滑到底之前请求数据,_.throttle
可以让我们不停的检查距离底部的距离。
1 | // 这是一个很简单的例子。 |
requestAnimationFrame (rAF)
requestAnimationFrame
是另外一种限频方式,可以看成_.throttle
(dosomething, 16),但是会精确很多,因为它是为了更好的精确度的浏览器原生API。
考虑下列优缺点后,我们可以酌情用rAF
API替代throttle
。
优点:
1、目标是60fps (16ms每帧),但内部将决定如何安排渲染的最佳时间;
2、非常简单且标准的API,将来不会改变,便于维护。
缺点:
1、我们需要开始或取消rAF,不像.debounce
或.throttle
自己在内部处理;
2、如果浏览器页面不是激活状态,rAF将不会执行,虽然对于滚动、鼠标、键盘事件这并不重要;
3、虽然所有的现代浏览器支持rAF,但是IE9、Opera Mini、和老的安卓并不支持,现在还需要打补丁;
4、node.js不支持rAF,所以不能用在服务端的文件系统事件。
一般来说,如果js函数需要重新计算元素位置,比如直接渲染或动效,我会用requestAnimationFrame
。发起数据请求,增加或移除控制动效的css class,我会用_.debounce
或_.throttle
,这样可以更低的执行频率,比如200ms,而不是16ms。
你或许觉得rAF
应该在underscore
或lodash
中实现,但他们都没有,因为它用途少并容易直接使用。
rAF示例
Paul Lewis写的的《Leaner, Meaner, Faster Animations with requestAnimationFrame》一步一步的介绍了这个例子的逻辑,受他的启发,这里我只会介绍requestAnimation
用在滚动上的例子。
我把_.throttle限频16ms和rAF放在一起对比,实现类似的功能,但极有可能rAF在复杂的场景会表现的更好。
1 | // 参考https://www.html5rocks.com/en/tutorials/speed/animations/#debouncing-scroll-events |
rAF的一个更好的例子我在headroom.js
库中看到过,它把逻辑解耦并封装。
总结
使用debounce
, throttle
和 requestAnimationFrame
来优化事件处理,它们略有不同,但都很有用,并相互补充。
方法 | 作用 |
---|---|
debounce |
把突然爆发的大量事件(比如连续快速的按键输入)组合成1个事件 |
throttle |
保证每X毫秒执行1次的持续的事件流。比如每200ms检查下滚动位置来触发CSS动效 |
requestAnimationFrame |
代替throttle 。当你在屏幕上重新计算并渲染元素,想保证变化或动效的流畅时使用。注意:不支持IE9 |