该篇为翻译文章,原文地址为Debouncing and Throttling Explained Through Examples

这篇文章的作者是 David Corbacho,伦敦的一名前端开发工程师。之前我们有一篇关于”节流”和”去抖”的文章:The Difference Between Throttling and Debouncing(译文:节流(Throttling)和去抖(Debouncing)的区别),但是David的这篇文章通过一些可交互的Demo来给我们做了一个更详细的解释。

“节流”和”去抖”都是用来限制一些需要一直执行的函数的技术,它们虽然很相似,但它们是不一样的。

当我们需要做一些DOM事件绑定时 ,”节流”和”去抖”是非常有用的。因为这样的话,我们在事件和事件函数之间又多了一层控制。所以,我们控制的不是事件的触发(事件触发的时机和频率),而是事件触发后去限制事件函数的执行。

例如我们非常常用的滚动事件:

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

当我们在电脑上进行滚动(用触控板、鼠标滚轮、拉动滚动条)的话,事件的触发可能会比较好控制一些,比如我想让它每秒触发30次,那么我滚动的慢一点,是可以达到的。但是,如果我在移动设备上(比如智能手机,iPad等设备)上进行滑动的话,每秒钟会触发100次左右。这很显然不是我们想要的结果,因为事件函数执行了太多次了。

在2011年,Twitter的网站出现过一个问题:当你滚动Twitter网站到底部进行加载数据时,页面将会变的非常卡,有时甚至未响应。John Resig写了一篇Learning from Twitter来说明了这个问题。

John当年(2011年)给出的解决方案是在滚动事件的外面以250毫秒为单位进行循环执行函数,这样函数和事件之间就实现了松耦合。用了这个方法,至少就不会影响到正常的用户体验了。

而现在,我们有一些更好更精细的方法来处理这个问题。下面,就让我们来介绍一下”去抖”、”节流”和”requestAnimationFrame()“方法。我们将通过对应的案例来进行详细的说明。

去抖

“去抖”可以让我们把一个连续的的函数调用”打包”成一个。
debounce

举个例子,假如你正在坐电梯,电梯门即将关上,这时突然有一个人想上电梯,于是电梯的门又开了。随后又来了一个人,同样的事又发生了一遍。电梯的根本功能是将你带到其它的楼层,而现在它由于很多人按电梯,所以到其它楼层这件事儿就被延误了,但是这样设计的原因是为了最终能节省资源。

你可以通过下面的例子来感受一下,点击下面的按钮或在上面进移动:

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

从上面的Demo中可以看到,”去抖”是如何工作的,它把本来一系列的事件触发变成了一个。但同时你也会发现,当我们的事件一直被触发时(例如上面的例子当中,用鼠标不停的在按钮上滑动),”去抖”就不起作用了。

‘去抖’前沿(去抖刚开始的时候)触发

“去抖”后函数只会在事件结束时触发,你可能会觉得”去抖”前的那段等待时间是非常不必要的。可是如果在一开始就触发函数那不是跟没有”去抖”一样了吗?其实不然,我们可以让函数在”去抖”的一开始就执行一次。

下面是’前沿’选项打开后的效果:

debounce-leading

underscore.js当中,可以通过把选项当中的leading换成immediate来实现。
(译者:在本段的上面和下面的例子当用的并非underscore,而是lodash)

下面是用lodash打开”前沿”触发后的效果:

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

‘去抖’的实现

我第一次见到在JS中实现”去抖”是John Hann在2009年写这篇Debouncing Javascript Methods

之后不久,Ben Alman编写了一个jQuery插件(该插件已经很久没更新了),一年后,Jeremy Ashkenas又将其加入了underscore.js,随后loadsh也加入了该功能。

See the Pen New example by Corbacho (@dcorb) on CodePen.

在上面所说的几种实现方式中,它们代码内部可能会有点小区别,但是使用方式都是差不多的。

以前underscore采用了和lodash一样的实现方式,但在2013年我发现了一个BUG。从那以后,它们则各自有自己的实现方式了。

Lodash 添加了一些其它功能在它的_.debounce_.throttle方法中。原来的immediate属性也被替换成了leadingtrailing。默认情况下,只有trailing是开启状态。

新的maxWait属性(目前只在Lodash当中有)在本文章当中没有说到,但它是一个非常有用的选项。实际上,throttle方法在内部就是调用了带有maxWait参数的_.debounce来实现的,你可以去看看lodash源代码

“去抖”实例

缩放实例

当缩放浏览器窗口大小的时候,会触发很多次resize事件。

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

上面的例子中,我们针对resize事件用了默认的选项(开启trailing),因为我们只关心缩放结束后的窗口大小。

“自动完成”(键盘按下时进行Ajax请求)实例

当用户进行键盘输入操作时就进行发送Ajax请求,这是不合理的,因为这样可能大概50毫秒就会发送一次请求。_.debounce方法会帮我们解决这个问题,用了该方法后,只有当我们停止输入的时候才会向后台发送Ajax请求。

这个例子也是不需要开启leading的。因为我们想要的是用户在输完最好一个字母时才触发函数。

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

如何使用”去抖”和”节流”以及它们的一些常见问题

我建议你使用underscore或者Lodash。如果你需要_.debounce_.throttle方法,你可以下载Lodash的自制版本,只需要一些简单的命令即可:

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

有一个常见的问题就是使用_.debounce两次:

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

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

如果用一个变量把”去抖”后的函数存储下来,那么我们可以通过调用debounced_version.cancel()来关闭这个”去抖”,在lodashunderscore.js中,你可以这样用。

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

// 如果有需要的话
debounced_version.cancel();

节流

使用_.throttle方法后会限制一个不得不需要一直执行的函数的执行频率,比如限制它每x毫秒执行一次。
其实,”节流”和”去抖”的最大区别就是,”节流”会保证函数一直在有规律的执行(至少每x毫秒一次的频率进行执行)。
debounce一样,throttle方法也集成在了underscore.jslodash当中。

‘节流’实例

‘无限滚动’实例

这是一个很常见的例子,就是当用户将页面滚动到将近底部时,发送一个Ajax请求,然后返回的数据添加到后面进行显示。
这个时候,上面所说到的_.debounce就不适用了,如果使用_.debounce的话,那么它只会在用户停止滚动时触发。但我们需要的是当用户快达到底部时触发。
使用_.throttle,我们可以保证一直监测用户滚动到了哪里。

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

requestAnimationFrame (rAF)

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

rAF就像_.throttle(dosomething, 16).方法。但是它更精确,因为它是浏览器内置的API。

我们可以使用rAF来替代throttle,下面来看一下它的优点和缺点。

优点:

  • rAF的刷新频率是`60fps`(每16毫秒刷新一次),但其实在内部,这个数字是不确定的,它会在适当的时候调为到比较适当的频率来进行渲染
  • 非常简单的API,一看就会

缺点:

  • rAFs的开始和结束是需要我们自己手动完成的,不像`.debounce`和`.throttle`会在内部自动进行完成
  • 如果浏览器窗口此时是未激活状态,它将不会执行。
  • 即可一些现代浏览器都支持了rAF,但是IE9、Opera Mini以及一些旧的安卓浏览器是不支持的。
  • nodeJS不支持rAF。

一般说来,rAF用在绘图和动画当中较多一点。如果是进行Ajax请求,或者进行添加、删除class(并会有一些CSS动画),那么用_.debounce_.throttle会更好。

rAF实例

下面的例子中,我将rAF和设置了参数为16ms_.throttle进行了对比( Paul Lewis的这篇Leaner, Meaner, Faster Animations with requestAnimationFrame有更详细的解释)。

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

结语

总之,你可以使用debounce,throttlerequestAnimationFrame去优化你的函数执行。每种方法都有一些细微的差别,但它们都非常有用。没有最好用的方法,只有更好用的方法。

  • 去抖-debounce:把一个一连串的事件函数(例如键盘输入)”打包”成一个进行执行
  • 节流-throttle:保证你的函数在一定频率下一直执行。例如当你滚动页面时,它会保证在每200ms检测一下你滚动的位置。
  • requestAnimationFrame: 可以看做是`throttle`的替代品。当你的函数有很多的动画渲染或者有很多的元素操作时,你想保证动画的流畅性,就需要用到这个。注意:IE9不支持rAF.