这篇文章的作者是 David Corbacho,伦敦的一名前端开发工程师。之前我们有一篇关于”节流”和”去抖”的文章:The Difference Between Throttling and Debouncing(译文:节流(Throttling)和去抖(Debouncing)的区别),但是David的这篇文章通过一些可交互的Demo来给我们做了一个更详细的解释。
“节流”和”去抖”都是用来限制一些需要一直执行的函数的技术,它们虽然很相似,但它们是不一样的。
当我们需要做一些DOM事件绑定时 ,”节流”和”去抖”是非常有用的。因为这样的话,我们在事件和事件函数之间又多了一层控制。所以,我们控制的不是事件的触发(事件触发的时机和频率),而是事件触发后去限制事件函数的执行。
例如我们非常常用的滚动事件:
当我们在电脑上进行滚动(用触控板、鼠标滚轮、拉动滚动条)的话,事件的触发可能会比较好控制一些,比如我想让它每秒触发30次,那么我滚动的慢一点,是可以达到的。但是,如果我在移动设备上(比如智能手机,iPad等设备)上进行滑动的话,每秒钟会触发100次左右。这很显然不是我们想要的结果,因为事件函数执行了太多次了。
在2011年,Twitter的网站出现过一个问题:当你滚动Twitter网站到底部进行加载数据时,页面将会变的非常卡,有时甚至未响应。John Resig写了一篇Learning from Twitter来说明了这个问题。
John当年(2011年)给出的解决方案是在滚动事件的外面以250毫秒为单位进行循环执行函数,这样函数和事件之间就实现了松耦合。用了这个方法,至少就不会影响到正常的用户体验了。
而现在,我们有一些更好更精细的方法来处理这个问题。下面,就让我们来介绍一下”去抖”、”节流”和”requestAnimationFrame()
“方法。我们将通过对应的案例来进行详细的说明。
去抖
举个例子,假如你正在坐电梯,电梯门即将关上,这时突然有一个人想上电梯,于是电梯的门又开了。随后又来了一个人,同样的事又发生了一遍。电梯的根本功能是将你带到其它的楼层,而现在它由于很多人按电梯,所以到其它楼层这件事儿就被延误了,但是这样设计的原因是为了最终能节省资源。
你可以通过下面的例子来感受一下,点击下面的按钮或在上面进移动:
从上面的Demo中可以看到,”去抖”是如何工作的,它把本来一系列的事件触发变成了一个。但同时你也会发现,当我们的事件一直被触发时(例如上面的例子当中,用鼠标不停的在按钮上滑动),”去抖”就不起作用了。
‘去抖’前沿(去抖刚开始的时候)触发
“去抖”后函数只会在事件结束时触发,你可能会觉得”去抖”前的那段等待时间是非常不必要的。可是如果在一开始就触发函数那不是跟没有”去抖”一样了吗?其实不然,我们可以让函数在”去抖”的一开始就执行一次。
下面是’前沿’选项打开后的效果:
在 underscore.js
当中,可以通过把选项当中的leading
换成immediate
来实现。
(译者:在本段的上面和下面的例子当用的并非underscore,而是lodash)
下面是用lodash打开”前沿”触发后的效果:
‘去抖’的实现
我第一次见到在JS中实现”去抖”是John Hann在2009年写这篇Debouncing Javascript Methods。
之后不久,Ben Alman编写了一个jQuery插件(该插件已经很久没更新了),一年后,Jeremy Ashkenas又将其加入了underscore.js,随后loadsh也加入了该功能。
在上面所说的几种实现方式中,它们代码内部可能会有点小区别,但是使用方式都是差不多的。
以前underscore采用了和lodash一样的实现方式,但在2013年我发现了一个BUG。从那以后,它们则各自有自己的实现方式了。
Lodash 添加了一些其它功能在它的_.debounce
和_.throttle
方法中。原来的immediate
属性也被替换成了leading
和trailing
。默认情况下,只有trailing
是开启状态。
新的maxWait
属性(目前只在Lodash当中有)在本文章当中没有说到,但它是一个非常有用的选项。实际上,throttle
方法在内部就是调用了带有maxWait
参数的_.debounce
来实现的,你可以去看看lodash源代码。
“去抖”实例
缩放实例
当缩放浏览器窗口大小的时候,会触发很多次resize
事件。
上面的例子中,我们针对resize
事件用了默认的选项(开启trailing
),因为我们只关心缩放结束后的窗口大小。
“自动完成”(键盘按下时进行Ajax请求)实例
当用户进行键盘输入操作时就进行发送Ajax请求,这是不合理的,因为这样可能大概50毫秒就会发送一次请求。_.debounce
方法会帮我们解决这个问题,用了该方法后,只有当我们停止输入的时候才会向后台发送Ajax请求。
这个例子也是不需要开启leading
的。因为我们想要的是用户在输完最好一个字母时才触发函数。
如何使用”去抖”和”节流”以及它们的一些常见问题
我建议你使用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()
来关闭这个”去抖”,在lodash
和underscore.js
中,你可以这样用。
var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);
// 如果有需要的话
debounced_version.cancel();
节流
使用_.throttle
方法后会限制一个不得不需要一直执行的函数的执行频率,比如限制它每x
毫秒执行一次。
其实,”节流”和”去抖”的最大区别就是,”节流”会保证函数一直在有规律的执行(至少每x
毫秒一次的频率进行执行)。
和debounce
一样,throttle
方法也集成在了underscore.js
和lodash
当中。
‘节流’实例
‘无限滚动’实例
这是一个很常见的例子,就是当用户将页面滚动到将近底部时,发送一个Ajax请求,然后返回的数据添加到后面进行显示。
这个时候,上面所说到的_.debounce
就不适用了,如果使用_.debounce
的话,那么它只会在用户停止滚动时触发。但我们需要的是当用户快达到底部时触发。
使用_.throttle
,我们可以保证一直监测用户滚动到了哪里。
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有更详细的解释)。
结语
总之,你可以使用debounce
,throttle
和requestAnimationFrame
去优化你的函数执行。每种方法都有一些细微的差别,但它们都非常有用。没有最好用的方法,只有更好用的方法。
- 去抖-debounce:把一个一连串的事件函数(例如键盘输入)”打包”成一个进行执行
- 节流-throttle:保证你的函数在一定频率下一直执行。例如当你滚动页面时,它会保证在每200ms检测一下你滚动的位置。
- requestAnimationFrame: 可以看做是`throttle`的替代品。当你的函数有很多的动画渲染或者有很多的元素操作时,你想保证动画的流畅性,就需要用到这个。注意:IE9不支持rAF.