节流(Throttling)和去抖(Debouncing)详解
这篇⽂章的作者是 ,伦敦的⼀名前端开发⼯程师。之前我们有⼀篇关于”节流”和”去抖”的⽂章:(译⽂:),但是David的这篇⽂章通过⼀些可交互的Demo来给我们做了⼀个更详细的解释。
“节流”和”去抖”都是⽤来限制⼀些需要⼀直执⾏的函数的技术,它们虽然很相似,但它们是不⼀样的。
occupation
当我们需要做⼀些DOM事件绑定时,”节流”和”去抖”是⾮常有⽤的。因为这样的话,我们在事件和事件函数之间⼜多了⼀层控制。所以,我们控制的不是事件的触发(事件触发的时机和频率),⽽是事件触发后去限制事件函数的执⾏。
例如我们⾮常常⽤的滚动事件:
当我们在电脑上进⾏滚动(⽤触控板、⿏标滚轮、拉动滚动条)的话,事件的触发可能会⽐较好控制⼀些,⽐如我想让它每秒触发30次,那么我滚动的慢⼀点,是可以达到的。但是,如果我在移动设备上(⽐如智能⼿机,iPad等设备)上进⾏滑动的话,每秒钟会触发100次左右。这很显然不是我们想要的结果,因为事件函数执⾏了太多次了。
在2011年,Twitter的⽹站出现过⼀个问题:当你滚动Twitter⽹站到底部进⾏加载数据时,页⾯将会变的⾮常卡,有时甚⾄未响应。写了⼀篇来说明了这个问题。
John当年(2011年)给出的解决⽅案是在滚动事件的外⾯以250毫秒为单位进⾏循环执⾏函数,这样函数和事件之间就实现了松耦合。⽤了这个⽅法,⾄少就不会影响到正常的⽤户体验了。
⽽现在,我们有⼀些更好更精细的⽅法来处理这个问题。下⾯,就让我们来介绍⼀下”去抖”、”节流”和”requestAnimationFrame()“⽅法。我们将通过对应的案例来进⾏详细的说明。
attempt
去抖
“去抖”可以让我们把⼀个连续的的函数调⽤”打包”成⼀个。
举个例⼦,假如你正在坐电梯,电梯门即将关上,这时突然有⼀个⼈想上电梯,于是电梯的门⼜开了。随后⼜来了⼀个⼈,同样的事⼜发⽣了⼀遍。电梯的根本功能是将你带到其它的楼层,⽽现在它由于很多⼈按电梯,所以到其它楼层这件事⼉就被延误了,但是这样设计的原因是为了最终能节省资源。
你可以通过下⾯的例⼦来感受⼀下,点击下⾯的按钮或在上⾯进移动:
从上⾯的Demo中可以看到,”去抖”是如何⼯作的,它把本来⼀系列的事件触发变成了⼀个。但同时你也会发现,当我们的事件⼀直被触发时(例如上⾯的例⼦当中,⽤⿏标不停的在按钮上滑动),”去抖”就不起作⽤了。
‘去抖’前沿(去抖刚开始的时候)触发
“去抖”后函数只会在事件结束时触发,你可能会觉得”去抖”前的那段等待时间是⾮常不必要的。可是如果在⼀开始就触发函数那不是跟没有”去抖”⼀样了吗?其实不然,我们可以让函数在”去抖”的⼀开始就执⾏⼀次。
下⾯是’前沿’选项打开后的效果:
在underscore.js当中,可以通过把选项当中的leading换成immediate来实现。
(译者:在本段的上⾯和下⾯的例⼦当⽤的并⾮underscore,⽽是lodash)
下⾯是⽤lodash打开”前沿”触发后的效果:
‘去抖’的实现
我第⼀次见到在JS中实现”去抖”是John Hann在2009年写这篇。
之后不久,Ben Alman编写了⼀个(该插件已经很久没更新了),⼀年后,Jeremy Ashkenas⼜将其,随后loadsh也加⼊了该功能。
在上⾯所说的⼏种实现⽅式中,它们代码内部可能会有点⼩区别,但是使⽤⽅式都是差不多的。
以前underscore采⽤了和lodash⼀样的实现⽅式,但在2013年我发现了。从那以后,它们则各⾃有⾃⼰的实现⽅式了。
Lodash 添加了⼀些其它功能在它的_.debounce和_.throttle⽅法中。原来的immediate属性也被替换成了leading和trailing。默认情况下,只有trailing是开启状态。
新的maxWait属性(⽬前只在Lodash当中有)在本⽂章当中没有说到,但它是⼀个⾮常有⽤的选项。实际上,throttle⽅法在内部就是调⽤了带
有maxWait参数的_.debounce来实现的,你可以去看看。
“去抖”实例
缩放实例
缩放实例
当缩放浏览器窗⼝⼤⼩的时候,会触发很多次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));
18 and life
如果⽤⼀个变量把”去抖”后的函数存储下来,那么我们可以通过调⽤debounced_version.cancel()来关闭这个”去抖”,在lodash和underscore.js中,你可以这样⽤。
var debounced_version = _.debounce(doSomething, 200);
$(window).on('scroll', debounced_version);
// 如果有需要的话elearning
debounced_version.cancel();
节流
使⽤_.throttle⽅法后会限制⼀个不得不需要⼀直执⾏的函数的执⾏频率,⽐如限制它每x毫秒执⾏⼀次。
其实,”节流”和”去抖”的最⼤区别就是,”节流”会保证函数⼀直在有规律的执⾏(⾄少每x毫秒⼀次的频率进⾏执⾏)。
和debounce⼀样,throttle⽅法也集成在了underscore.js和lodash当中。
‘节流’实例
‘⽆限滚动’实例
这是⼀个很常见的例⼦,就是当⽤户将页⾯滚动到将近底部时,发送⼀个Ajax请求,然后返回的数据添加到后⾯进⾏显⽰。
这个时候,上⾯所说到的_.debounce就不适⽤了,如果使⽤_.debounce的话,那么它只会在⽤户停⽌滚动时触发。但我们需要的是当⽤户快达到底部时触发。
使⽤_.throttle,我们可以保证⼀直监测⽤户滚动到了哪⾥。
skyy
requestAnimationFrame (rAF)
requestAnimationFrame是另⼀种限制函数执⾏频率的⽅法。
rAF就像_.throttle(dosomething, 16).⽅法。但是它更精确,因为它是浏览器内置的API。
我们可以使⽤rAF来替代throttle,下⾯来看⼀下它的优点和缺点。
优点:
ro jackrAF的刷新频率是`60fps`(每16毫秒刷新⼀次),但其实在内部,这个数字是不确定的,它会在适当的时候调为到⽐较适当的频率来进⾏渲染
⾮常简单的API,⼀看就会
缺点:
rAFs的开始和结束是需要我们⾃⼰⼿动完成的,不像`.debounce`和`.throttle`会在内部⾃动进⾏完成
如果浏览器窗⼝此时是未激活状态,它将不会执⾏。
即可⼀些现代浏览器都⽀持了rAF,但是IE9、Opera Mini以及⼀些旧的安卓浏览器是不⽀持的。
suppodly
nodeJS不⽀持rAF。
欢迎的英文
⼀般说来,rAF⽤在绘图和动画当中较多⼀点。如果是进⾏Ajax请求,或者进⾏添加、删除class(并会有⼀些CSS动画),那么
⽤_.debounce或_.throttle会更好。
rAF实例
下⾯的例⼦中,我将rAF和设置了参数为16ms的_.throttle进⾏了对⽐( Paul Lewis的这篇有更详细的解释)。
结语
总之,你可以使⽤debounce,throttle和requestAnimationFrame去优化你的函数执⾏。每种⽅法都有⼀些细微的差别,但它们都⾮常有⽤。没有最好⽤的⽅法,只有更好⽤的⽅法。
去抖-debounce:把⼀个⼀连串的事件函数(例如键盘输⼊)”打包”成⼀个进⾏执⾏
节流-throttle:保证你的函数在⼀定频率下⼀直执⾏。例如当你滚动页⾯时,它会保证在每200ms检测⼀下你滚动的位置。
requestAnimationFrame: 可以看做是`throttle`的替代品。当你的函数有很多的动画渲染或者有很多的元素操作时,你想保证动画的流畅性,就需要⽤到这个。注意:IE9不⽀持rAF.