Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【译】以案例阐述 Debounce 和 Throttle #34

Open
JChehe opened this issue Nov 6, 2018 · 0 comments
Open

【译】以案例阐述 Debounce 和 Throttle #34

JChehe opened this issue Nov 6, 2018 · 0 comments

Comments

@JChehe
Copy link
Owner

JChehe commented Nov 6, 2018

原文:Debouncing and Throttling Explained Through Examples

DebounceThrottle 两者很类似(但不同!),均用于控制函数在一定时间范围内的执行频率。

将 debounce 或 throttle 后的函数用于 DOM 事件绑定是非常有用的。为什么?因为这让我们在事件和函数调用之间拥有了控制权。毕竟我们不能控制 DOM 事件的触发频率,却可以控制回调函数的执行频率。

例如,以下是 scroll 事件:

See the Pen Scroll events counter by Corbacho (@dcorb) on CodePen.

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

当通过触摸板、鼠标滚轮或拖拽滚动条时,事件在 1 秒内的触发次数能轻松达到 30 次。智能手机就更甚了,在我们的测试中,缓慢滚动也能在 1 秒内触发事件次数到 100 次。而你的滚动回调函数是否已对此执行频率做好准备呢?

在 2011 年,Twitter 网站出现了一个问题:当往下滚动信息流时,网站的响应速度会变慢,甚至是拒绝响应。John Resig 写了一篇 关于该问题的文章,其阐述了直接为 scroll 事件绑定耗时函数的严重性。

John 的建议(五年前)是:onScroll 事件的回调函数应该每 250ms 执行一次。这样回调函数就不会直接耦合到事件。使用这种简单的技术就可以避免破坏用户体验。

如今,处理事件的方式需要变得更复杂一些。接下来,我会结合案例向大家介绍 Debounce、Throttle 和 requestAnimationFrame。

Debounce

Debounce 技术让多次序列调用“结合”为一次。

debounce

假如你在电梯里,门开始关闭,突然有人想进来。此时,电梯不会开始执行改变楼层的功能,门再次打开。当再有另一个进来则会重复这个步骤。尽管电梯延迟了上下移动的行为,但却优化了电梯资源。

亲自尝试一下吧,点击或在按钮上移动:

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

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

你可以看到快速连续触发的事件是如何结合为一个的 debounce 事件呈现。但如果事件的触发间隔较大,则呈现不出 debounce 的效果。

提前(或“立刻”)

提前执行 debounce 的案例
提前(或称为立刻)执行 debounce 的案例

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

亲自尝试一下:

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

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

Debounce 的实现

我第一次看到 debounce 的 JavaScript 实现是在 2009 年的 John Hann 文章(他也是该术语的创造者)。

不久之后,Ben Alman 开发了一个 jQuery 插件(不再维护)。一年后,Jeremy Ashkenas 将 其添加到了 underscore.js。而 underscore 的替代方案 Lodash 也随后添加。

这 3 种实现均有一些不同,但接口几乎一致。

有一段时间,underscore 采用了 Lodash 的 debounce/throttle 的实现,但随后我在 2013 年发现了 _.debounce 的一个 Bug。从那时起,两者就分开各自实现了。

Lodash 为 _.debounce_.throttle 函数 添加了更多特性。原来的 immediate 标识被替换成 leadingtrailing 可选项。该两个选项可开启一项或同时开启。默认情况下,仅 trailing 开启。

新可选项 maxWait(当时仅 Lodash 支持)并未在本文涵盖,但它十分有用。实际上,throttle 函数是通过 _.debouncemaxWait 实现的,详情可查看 Lodash 源码

Debounce 案例

Resize 案例

当拖拽改变浏览器窗口尺寸时,会触发非常多次 resize 事件。

如以下案例:

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

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

如你所见,我们为 resize 事件使用了默认的 trailing 选项。毕竟,我们只对最后的值感兴趣(用户停止调整浏览器尺寸)。

用 Ajax 自动完成键入

有什么理由在用户仍在输入时每隔 50ms 发起 Ajax 请求呢?_.debounce 能帮助我们避免额外的操作,仅在用户停止输入时发起请求。

对于这个案例,leading 标识是没意义的,毕竟我们只想等到输入的最后一个字母结束。

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

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

类似的案例是等到用户停止键入时进行校验,然后弹出诸如“您的密码太短”的消息提示。

如何使用 debounce 和 throttle 并避免常见陷阱

编写属于自己的 debounce/throttle 函数看似很诱人,或者随便从博客文章中复制使用。而我个人的推荐是直接使用 underscore 或 Lodash。如果你仅需要 _.debounce_.throttle 函数,那么可以使用 Lodash 的自定义构建方式生成 2KB 的库。通过以下简单的命令行构建:

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

结合 webpack/browserify/rollup 构建工具,引入相应模块: loadsh/throttlelodash/debounce 或者 lodash.throttlelodash.debounce

一个常见的陷阱是多次调用 _.debounce 函数:

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

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

将 debounce 后的函数赋值到一个变量,即可在需要的时候调用私有方法 debounced_version.cancel()。这适用于 lodash 和 underscore.js。

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

// 需要的时候
debounced_version.cancel();

Throttle

通过使用 _.throttle,我们可以避免函数的执行频率过高(即每 X 秒大于一次)。

这与 debounce 的最大区别是:throttle 能保证函数能定期执行。即 X 毫秒内至少一次,而对于 debounce,只要一直保持高频繁触发事件,那么回调函数就一直不会被执行。

与 debounce 相同的是,throttle 技术均在 Ben 的插件、underscore.js 和 lodash 上提供。

Throttle 案例

无限滚动

这是一个十分常见的案例。用户在可无限滚动的页面中往下滚动时,你需要检测用户当前距离底部的距离。如果接近底部,那么就应该通过 Ajax 请求更多的内容,并将内容插入到页面中。

对于这种情况,_.debounce 并不能帮上忙,这是因为它只能等到用户停止滚动时才能调用回调函数。而我们这里需要在用户到达底部前就开始获取内容了。

通过 _.throttle,我们能保证不间断地检查用户到底部的距离。

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

<script async src="https://static.codepen.io/assets/embed/ei.js"></script>

requestAnimationFrame (raF)

requestAnimationFrame 是另一种限制函数执行频率的方式。

它可以被看作为 _.throttle(dosomething, 16)。但其拥有更高的精确性,毕竟它是旨在提供更高精度的浏览器原生 API。

综合其优缺点,我们可以使用 rAF API 作为 throttle 的替代方案:

优点:

  • 目标是达到 60fps(每帧 16ms),但浏览器内部会安排好渲染的最佳时机。
  • 相当简单的标准 API,未来不会更改,减少维护成本。

缺点:

  • .debounce.throttle 不同的是,我们只能对 rAF 发出 启动/取消的指令,但其终归浏览器内部管理。
  • 如果浏览器标签不处于激活状态,那它将不会执行。尽管这对滚动、鼠标和键盘事件来说并不重要。
  • 尽管所有现代浏览器都提供 rAF,但 IE9、Opera Mini 和老旧的 Android 并不支持。在今天仍 可能需要 polyfill
  • Node.js 不支持 rAF,因此不能在服务器对文件系统事件 进行 throttle 优化。

根据经验,如果 JavaScript 函数是用于“绘制”或直接过渡动画属性,那么就用 requestAnimationFrame。总之,在涉及重新计算元素位置的时候就该使用它。

对于 Ajax 请求或决定是否添加/删除类名(用于触发 CSS 动画)时,我会偏向于 _.debounce_.throttle,毕竟能设置更低的执行频率(比如 200ms,而不是 16ms)。

你可能会想到:rAF 应该集成到 underscore 或 lodash 中,但他们均拒绝了这个想法。毕竟它更多是作为一个特定案例,并且很容易被直接调用。

rAF 案例

我仅讨论以下这个案例:在滚动时使用 requestAnimationframe。这个案例的灵感来自 Paul Lewis 的文章,这篇文章细致地解释了这个案例的逻辑。

我将 rAF 与 16ms 的 _.throttle 并排比较。尽管性能看似相近,但 rAF 能在更复杂的场景中为你提供更佳的性能。

我见过使用该技术的一个更高级的例子是:headroom.js 库。它的实现 逻辑被解耦 包装在一个对象中。

总结

使用 debounce、throttle 和 requestAnimationFrame 能优化事件回调函数。尽管三种技术略有不同,但它们都十分有用并相互补充。

总的来说:

  • debounce:将一堆突发事件(如键入)结合为一个事件。
  • throttle:保证每 X 毫秒执行一次固定流程。比如滚动时每 200ms 检查滚动位置来决定是否触发 CSS 动画。
  • requestAnimationFrame:throttle 的替代方案。当函数涉及重新计算或渲染元素时要保证动画和更改的流畅性,那么就适合使用它。注意:IE9 不支持。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant