junerzyz

通过例子来对比Debouncing,Throttling,requestAnimationFrame | CSS-Tricks

junerzyz · 2017-02-07翻译 · 383阅读 原文链接

以下这篇文章分享自 David Corbacho,伦敦的一名前端开发工程师. 我们曾经分享过 关于Debouncing,Throttling这两个概念的一篇文章,但是这次的分享,David将通过几个交互例子来深入其概念,让读者更加清楚。

Debounce and throttle 是两个相似但不完全相同的概念,它们同样都是去控制我们之后对某个函数要执行多少次。

当我们给DOM事件绑定函数时,给函数增加debounced或者throttled是非常有用的。为什么呢?因为我们在事件和函数执行之间给自己提供了一层控制。记住,我们不控制DOM事件多少次被触发,因为它是变化莫测的。

举个例子,来谈谈滚动(scroll)事件。请查看这个下面的codepen: Scroll events counter by Corbacho (@dcorb) on CodePen.

当我们用触摸板,滚轮或者只是拖拽滚动条,就能轻易在每秒触发30次事件。而通过测试,在手机缓缓地滚动则可以轻易地在每秒触发100次。你确定你的滚动处理程序能够应付这种执行速率吗?

在2011年,一个issue被Twitter所提:当你在Twitter反馈进行页面滚动时,页面的反应会很慢和出现卡顿现象。John Resing发表了一篇博文来阐述这个问题,解释了直接给scroll绑定事件的行为是多么的糟糕。 而John(在五年前)建议的解决方案是在onScroll事件之外每250ms运行一次循环. 这样的方法不会导致与事件的耦合.通过这个简单的实现,我们就可以避免较差的用户体验。

之后,也出现了其它的解决方案。让我来介绍一下Debounce,Throttle,以及requestAnimationFrame。我们会通过实际用例来解释它们。

Debounce

Debounce允许我们在一个单独的“分组”来进行有序地调用。

想象你在一个电梯。在门开始关闭的时候,突然有另外一个人想要进来。电梯门会再次开启,等到这个人进来后再进行升降。如果再有另外一个人想进来。电梯就会延迟执行(升降),以此来优化资源的利用率。

你可以自己动手试试,点击或者将鼠标从顶部移动到底部。

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

你可以看到在一个单独的debounced事件里事件的执行时刻。但是如果事件在一个很长的距离再次触发,debouncing就不会发生。

Leading选项 (or "immediate")

你可能会发现令人讨厌的是,debouncing事件在事件停止快速触发之前它不会立刻触发要执行的函数。为什么不能像原先不用debounced处理一样立刻执行函数呢(在正常非频繁触发事件的操作)?但使用上面默认的方法确实会在停止快速触发事件时延迟触发一次正常的操作。

但也不是没有方法!以下就是用leading选项来解决这个问题的示例:

在underscore.js,这个选项叫做immediate而不是leading.

建议自己尝试一下:

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

Debounce实现

我第一次看见debounce在js的实现是2009年看的一篇文章this John Hann post (也是这个作者创造了debounce这个术语).

之后很快的, Ben Alman 创建了一个插件a jQuery plugin(现在已经不再维护了)。一年之后, Jeremy Ashkenas 在underscore.js也实现了debounce。很快Lodash也支持, 它是与underscore极为相似的一个库.

这三种方式在内部实现有一点微小的区别,但是它们的接口几乎完全一样。

underscore采用Lodash的debounce/throttle有一段时间了,但自从2013年在_.debounce 方法出现了[一个bug]。(http://drupalmotion.com/article/debounce-and-throttle-visual-explanation)后,它们两个就各自采用了不同的实现。

Lodash给_.debounce_.throttle方法增添了更多的特性。原本的immediate标记被leadingtrailing选项取代了。你可以利用这两个选项实现你想要的效果。但默认trailing为true。

新的选项maxWait选项(只有Lodash有)并不在本章讨论的范围内,但是它可以很有用。事实上,throttle就是用了maxWait_.debounce实现,具体实现你可以在lodash源码里面看到。

Debounce使用例子

示例1:Resize

当在调整浏览器窗口大小的时候,确切说是在你拖拉以便调整到合适大小的过程resize事件会被触发很多次。

可以看下面的这个demo:

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

你在上面可以看到,当我们使用默认的trailing选项时,因为我们只对最后的结果感兴趣,所以用户停止调整窗口的瞬间是至关重要的。

用Ajax实现键盘输入自动填充

为什么要在用户还在输入的时候选择没50ms给服务器发送Ajax请求呢?_.debounce可以帮助我们减少不必要的工作,让用户在停止输入的时候才发送请求。

在这里,就没必要开启leading属性了。我们只想在乎最后一个字母被输入的时候后最终的结果。

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

同样的,你可以用类似的方法实现在用户停止输入时才去进行输入校验。

怎么使用debounce和throttle以及有哪些常见的陷阱

你可能会想自己码一个debounce/throttle方法,或者随便从某个地方copy下来。我的建议是直接使用underscore或者Lodash.如果你只需要_.debounce_.throttle这两个方法,你可以使用Lodash的自定义构建工具去得到一个自定义(需要的方法)且压缩后只有2KB的库。用以下的命令即可构建:

npm i -g lodash-cli
lodash-cli 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函数创建一个变量可以允许我们调用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

通过使用-.throttle,我们可以在每X毫秒去执行一次函数。

它和debouncing的主要区别就在于throttle保证方法更加有规律地执行,即至少会在每X毫秒执行一次。

同样的,throttle也在Ben的那个插件里,在underscore.js和lodash都得到了实现。

Throttling示例

无限滚动

一个最常见的例子就是用户可以在一个页面进行无限滚动。你需要检查用户滚动的距离,如果在用户快达到底部了,我们应该发一次异步请求来请求更多的数据并把它加入到页面中。

在这里我们深爱的_.debounce派不上用处,因为它只会在用户停止滚动的触发...而我们需要在用户停止触发之前就去获取数据了。

使用_.throttle,我们可以不断检查我们离底部还有多远。

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

requestAnimationFrame (rAF)

requestAnimationFrame 是另外一个限制执行频率的方法。

它可以被看作是_.throttle(dosomething,16)。但是它有更高的保真性,因为他是浏览器的原生API,准确率也更得到了保障。

我们可以使用rAF API来作为throttle的另外一个选择,它的优缺点有:

Pros

  • 旨在达到60ftp(没帧16ms),但内部可以自行决定如何安排最佳的渲染时机。

  • 相当简单和标准的API,至少在未来不会发生太大的变化,更易于维护性。

Cons

  • 不同于.debounce或者.throttle,我们需要自己启动或者取消rAF。

  • 如果浏览器没有激活这个选项,它不会被执行。即使是在滚动,鼠标或者键盘事件也一样。

  • 虽然所有现代浏览器度支持rAF,它在IE9,Opera Mini和旧版本的安卓还是不被支持。 为了兼容所有浏览器,还是建议引入polyfill文件或者查一下你面向的浏览器是否可以支持它

  • rAF不能在node.js里面运行,所以你不能在服务端去限制文件系统事件。

作为一个经验法则,如果你的JavaScript方法是在“绘制”或者改变动画属性,建议用requestAnimationFrame在任何频繁地进行重绘元素位置的地方。

为了使用异步请求,或者决定是否要添加/移除(会触发CSS动画的)类名,我会考虑用_.debounce或者_.throttle,你可以降低其执行间隔(假如用200ms来取代16ms)

如果你可以通过在underscore或者lodash内部去实现rAF,那不会是个好主意。因为它是个特殊的用例,也很容易被直接调用。

rAF示例

我只会在这个例子中在scroll去使用requestAnimationFrame,灵感来源于Paul Lewis article,它一步步解释了这个例子的逻辑。

我把它拿来和16ms的_.throttle进行比较。虽然在本例它们的性能不相上下,但是rAF在更加复杂的场景可能会给你更好的结果。

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

一个更复杂一点的例子是headroom.js,它把它封装在一个对象以达到解耦的效果。headroom.js

总结

使用debounce,throttle和requestAnimationFrame去优化你的事件处理吧。每个技术都有不一样的地方,但是他们三个都是非常有用并且相得益彰。

归纳以下几点:

  • debounce: 可以把重复的事件(尤其是只在意结果不在意过程的频繁事件)进行分组(比如键盘输入时间)。

  • throttle: 保证执行可以在特定的时间执行至少一次,比如你想要在每200ms的时候去触发一个CSS动画。

  • requestAnimationFrame: throttle的另一个选择。当你的方法是在计算并且渲染屏幕上的元素而你又想得到流畅的体验时你应该要想到它. 注意:IE9不支持。

译者junerzyz尚未开通打赏功能

相关文章