【译】经过例子解释 Debounce 和 Throttle

DebounceThrottle 是两个很类似可是又不一样的技术,均可以控制一个函数在一段时间内执行的次数。javascript

当咱们在操做 DOM 事件的时候,为函数添加 debounce 或者 throttle 就会尤其有用。为何?由于咱们在事件和函数执行之间加了一个咱们本身的控制层。记住,咱们是不去控制这些 DOM 事件触发的频率的,由于这个可能会有变化。css

下面咱们以滚动事件举例:html

<iframe height='265' scrolling='no' title='Scroll events counter' src='//codepen.io/athena0304/embed/Yjbqar/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Scroll events counter.
</iframe>html5

当使用触控板、鼠标滚轮,或者直接拽动滚动条,每秒均可以轻易触发至少30次事件,并且在触屏的移动端,甚至会达到每秒100次,面对这样高的执行频率,你的滚动事件处理程序可否很好地应对?java

在2011年,Twitter 网站提出了一个 issue:当向下滚动 Twitter 信息流的时候,整个页面的响应速度都会变慢。 John Resig 基于该问题发表了一篇博客,文中指出,直接在 scroll 事件里挂载一些计算量大的函数是件多么不明智的行为。jquery

John 当时提出的解决方案是在 onScroll event 的外部设置一个每 250ms 执行一次的循环。这样处理程序就与事件解耦了。使用这样一个简单的技术就能够避免破坏用户体验。webpack

::: tip 译者注git

文中的核心代码以下github

var outerPane = $details.find(".details-pane-outer"),
    didScroll = false;

$(window).scroll(function() {
    didScroll = true;
});

setInterval(function() {
    if ( didScroll ) {
        didScroll = false;
        // Check your page position and then
        // Load in more results
    }
}, 250);

:::web

现在,处理事件的方式稍微复杂了一些。下面咱们结合用例,一一介绍 Debounce、 Throttle 和requestAnimationFrame。

Debounce

Debounce 容许咱们将多个连续的调用合并成一个。

img

想象一个进电梯的场景,你走进了电梯,门刚要关上,这时另外一我的想要进来,因而电梯没有移动楼层(处理函数),而是将门打开让那我的进来。这时又有一我的要进来,就又会上演刚才那一幕。也就是说,电梯延迟了它的函数(移动楼层)执行,可是优化了资源。

在下面的例子中,尝试快速点击按钮或者在上面滑动:

<iframe height='265' scrolling='no' title='Debounce. Trailing' src='//codepen.io/athena0304/embed/NBVjRB/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce. Trailing.
</iframe>

你能够看到连续快速事件是怎样被一个单独的 debounce 事件所替代的。可是若是事件触发时间间隔较长,就不会发生 debounce。

Leading 边缘 (或者 "immediate")

在上面的例子中,你会发现 debounce 事件会等到快速事件中止发生后才会触发函数执行。为何不在每次一开始就当即触发函数执行呢,这样它的表现就和原始的没有去抖的处理器同样了。直到快速调用出现停顿的时候,才会再次触发。

下面是使用 leading 标识符的例子:

img

在 underscore.js 中,该选项叫做 immediate ,而不是 leading

本身试一下:

<iframe height='265' scrolling='no' title='Debounce. Leading' src='//codepen.io/athena0304/embed/mGbgGo/?height=265&theme-id=0&default-tab=css,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce. Leading.
</iframe>

Debounce 的实现

Debounce 的概念和实现最先是由 John Hann 在2009年提出来的。

不久以后,Ben Alman 就写了一个 jQuery 插件(如今已经再也不维护了),一年以后 Jeremy Ashkenas 把它添加进了 underscore.js。再后来被添加进 Lodash。

这三个实如今内部有一点不一样,可是接口几乎是相同的。

曾经有一段时间,underscore 采起了 Lodash 里面的 debounce/throttle 实现,可是后来我在2013年发现了 _.debounce 函数的一个 bug。从那时起,这两种实现就出现分化了。

Lodash 为 _.debounce_.throttle 添加了更多的特性。最初的 immediate 标识符被 leadingtrailing 所替代。你能够选择一个选项,也能够两个都要。默认状况下 trailing 是被开启的。

新的 maxWait 选项(目前只存在于Lodash)在本文中没有说起,可是它也是一个颇有用的选项。实际上,throttle 函数就是使用 _.debounce 带着 maxWait 的选项来定义的,你能够在这里查看源码

Debounce 举例

Resize 举例

经过拖拽浏览器窗口,能够触发不少次 resize 事件。

例子以下:

<iframe height='265' scrolling='no' title='Debounce Resize Event Example' src='//codepen.io/athena0304/embed/KxPLZy/?height=265&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debounce Resize Event Example.
</iframe>
能够看到,咱们在 resize 事件上使用的是默认的 trailing 选项,由于咱们只须要关心用户中止调整浏览器后的最终结果就能够了。

敲击键盘,经过 Ajax 请求自动填充表单

为何要在用户还在输入的时候每隔 50ms 就发送一次 Ajax请求?_.debounce 能够帮助咱们避免额外的开销,只有当用户中止输入了再发送请求。

这里没有必要设置 leading,咱们是想要等到最后一个字符输入完再执行函数的。

<iframe height='265' scrolling='no' title='Debouncing keystrokes Example' src='//codepen.io/athena0304/embed/NLKVZw/?height=265&theme-id=0&default-tab=js,result&embed-version=2' frameborder='no' allowtransparency='true' allowfullscreen='true' style='width: 100%;'>See the Pen Debouncing keystrokes Example.
</iframe>
还有一个相似的使用场景就是表单校验,当用户输入完再进行校验、提示信息等。

如何使用 debounce 和 throttle,以及常见问题

说了这么多,你可能已经想本身来写 debounce/throttle 函数了,或者是从网上随便一篇博客上拷贝一份下来。可是我给你的建议是直接使用 underscore 或者 Lodash。 若是你只是须要 _.debounce_.throttle 函数,可使用 Lodash custom builder 来输出一个自定义的压缩后为 2KB 的库。可使用下列命令来进行构建:

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

也就是说,最好是使用模块化的形式,经过 webpack/browserify/rollup 来引用,如 lodash/throttlelodash/debouncelodash.throttlelodash.debounce

使用 _.debounce 函数的一个常见错误就是屡次调用它:

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

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

为 debounced 函数建立一个变量可让咱们调用私有函数 debounced_version.cancel(),若是有须要,lodash 和 underscore.js 均可以供你使用。

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

// 若是你须要的话
debounced_version.cancel();

Throttle

使用 _.throttle 则不容许函数每 X 毫秒的执行次数超过一次。

Throttle 和 debounce 最主要的区别就是 throttle 保证函数每 X 毫秒至少执行一次。

和 debounce 同样, throttle 也用在了 Ben 的插件、underscore.js 和 lodash里面。

Throttling 举例

无限滚动

这是一个很是常见的例子。用户在一个无限滚动的页面里向下滚动,你须要知道当前滚动的位置距离底部还有多远,若是接近底部了,咱们就得经过 Ajax 请求获取更多的内容,将其添加到页面里。

此时咱们以前的 _.debounce 就派不上做用了。使用 debounce 只有当用户中止滚动时才能触发,而咱们须要的是在用户滚动到底部以前就开始获取内容。

使用 _.throttle 就能确保实时检查距离底部还有多远。

requestAnimationFrame (rAF)

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

它能够被看作 _.throttle(dosomething, 16)。但它有着更高的保真度,由于它是浏览器的原生 API,有着更好的精度。

咱们可使用 rAF API,做为 throttle 函数的替代,考虑下面的优缺点:

优势

  • 目标是 60fps(每帧 16ms),可是会在浏览器内部决定如何安排渲染的最佳时机。
  • 很是简单,并且是标准 API,在将来也不会改变。更少的维护成本。

缺点

  • rAFs 的开始/取消由咱们本身来管理,而不像 .debounce.throttle 是在内部管理的。
  • 若是浏览器的 tab 页面不活跃了,它就不会再执行。
  • 虽然全部的现代浏览器都提供了 rAF, 可是 IE九、Opera Mini 和一些老的安卓版本还不支持。若是须要,如今仍是要使用 polyfill
  • Node.js 不支持 rAF,因此不能在服务端用于 throttle 文件系统事件。

根据经验,若是你的 JavaScript 函数是在绘制或者直接改变属性,全部涉及到元素位置从新计算的,我会建议使用 requestAnimationFrame

若是是处理 Ajax 请求,或者决定是否添加/删除某个 class(可能会触发一个 CSS 动画),我会考虑 _.debounce_.throttle,这里能够设置更低一些的执行速度(例如 200ms,而不是16ms)。

这时你可能会想,为什不把 rAF 集成到 underscore 或 lodash 里呢,那他俩都是拒绝的,由于这只是一个特殊的使用场景,并且已经足够简单,能够被直接调用。

rAF 举例

这篇文章的启发,在这里我会举一个滚动的例子,在这篇文章中有每一个步骤的逻辑解释。

我作了一个对比实验,一边是 rAF,一边是 16ms 间隔的 _.throttle。它们性能很类似,可是 rAF 可能会在更复杂的场景下性能更高一些。

<p data-height="265" data-theme-id="0" data-slug-hash="VGKMWv" data-default-tab="js,result" data-user="athena0304" data-pen-title="Scroll comparison requestAnimationFrame vs throttle" class="codepen">See the Pen Scroll comparison requestAnimationFrame vs throttle</p>
<script async src="https://static.codepen.io/ass...;></script>

还有一个更高级的例子,在 headroom.js 中,逻辑被解耦了,而且包裹在了对象中。

总结

使用 debounce、throttle 和 requestAnimationFrame 来优化你的事件处理程序。每种技术都有些许的不一样,可是三个都是颇有用的,并且可以互补。

总结:

  • debounce:将一系列迅速触发的事件(例如敲击键盘)合并成一个单独的事件。
  • throttle:确保一个持续的操做流以每 X 毫秒执行一次的速度执行。例如每 200ms 检查一下滚动条的位置来触发某个 CSS 动画。
  • requestAnimationFrame:throttle的一个替代品。适用于须要计算元素在屏幕上的位置和渲染的时候,可以保证动画或者变化的平滑性。注意:IE9 不支持。

原文连接:https://css-tricks.com/deboun...

相关文章
相关标签/搜索