资讯专栏INFORMATION COLUMN

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

LeoHsiun / 2219人阅读

摘要:举例举例通过拖拽浏览器窗口,可以触发很多次事件。不支持,所以不能在服务端用于文件系统事件。总结将一系列迅速触发的事件例如敲击键盘合并成一个多带带的事件。确保一个持续的操作流以每毫秒执行一次的速度执行。

DebounceThrottle 是两个很相似但是又不同的技术,都可以控制一个函数在一段时间内执行的次数。

当我们在操作 DOM 事件的时候,为函数添加 debounce 或者 throttle 就会尤为有用。为什么?因为我们在事件和函数执行之间加了一个我们自己的控制层。记住,我们是不去控制这些 DOM 事件触发的频率的,因为这个可能会有变化。

下面我们以滚动事件举例:

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

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

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

::: tip 译者注

文中的核心代码如下

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);

:::

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

Debounce

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

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

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

你可以看到连续快速事件是怎样被一个多带带的 debounce 事件所替代的。但是如果事件触发时间间隔较长,就不会发生 debounce。

Leading 边缘 (或者 "immediate")

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

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

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

自己试一下:

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 事件。

例子如下:


可以看到,我们在 resize 事件上使用的是默认的 trailing 选项,因为我们只需要关心用户停止调整浏览器后的最终结果就可以了。

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

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

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


还有一个类似的使用场景就是表单校验,当用户输入完再进行校验、提示信息等。

如何使用 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, 但是 IE9、Opera Mini 和一些老的安卓版本还不支持。如果需要,现在还是要使用 polyfill 。

Node.js 不支持 rAF,所以不能在服务端用于 throttle 文件系统事件。

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

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

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

rAF 举例

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

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

See the Pen Scroll comparison requestAnimationFrame vs throttle


阅读需要支付1元查看
<