01.前端高级-JavaScript进阶 > 3.函数式编程 Underscore源码分析 > 3.4.3 throttle 与 debounce 概念解析源码实现
CoderMonkie
1.认识throttle(节流)与debounce(防抖)
throttle(节流)与debounce(防抖)
throttle和debounce是解决请求和响应速度不匹配问题的两个方案。
二者的差异在于选择不同的策略。
debounce的关注点是空闲的间隔时间,
throttle的关注点是连续的执行间隔时间。
应用场景
只要涉及到连续事件或频率控制相关的应用就可以考虑使用这两个函数,比如:
- 游戏设计,
keydown事件 - 文本输入、自动完成,
keyup事件 - 鼠标移动
mousemove事件 DOM元素动态定位,window对象的resize、scroll事件
前两者,debounce和throttle都可以按需使用;
后两者就要用throttle了
(以上摘自网易云课堂微专业前端高级课程)
上手试试
很容易想到的应用场景就是,用户输入,
wait间隔毫秒数,就是用户操作停顿的事件,
如果间隔非常小,就视为没有停顿;
如果超过设定的值,就认为停顿了,发起web请求。
但我们这里用页面滚动scroll事件来举例(观测简便)。
没有防抖与节流的操作
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>debounce sample</title>
</head>
<body>
<div style="height: 1500px;"></div>
<script>
// scroll in normal
window.addEventListener('scroll', function(){
console.log('normal scrolling...')
})
</script>
</body>
</html>
打开样例页面的控制台并滚动鼠标滚动轮,会发现随着滚动不停地打印出log信息。
debounce
-
原理
对于设定的间隔时间(通常为毫秒)内的交互,只响应最新的,之前的全部忽略掉。
用一个形象的例子来说明:
用户(比如你)跟baidu说,我想搜索点不可告人的学习资料,
baidu:你确定吗?1500毫秒内不回复的话,我就检索了
--未超过1500毫秒,重新/继续 输入--
用户(比如你):更改搜索内容为“JS中防抖节流的原理与应用”
baidu:你确定吗?1500毫秒内不回复的话,我就检索了
--超过1500毫秒未输入--
baidu:已发起检索请求,这是结果(JS中防抖节流的原理与应用)
--此时再输入--
用户(比如你):我还是想看点令人愉悦的龌龊内容
--回到对话开始时的状态,以下重复省略--
1500毫秒只是打个比方,实际比这个值要小(其实在搜索中,防抖与节流都有使用,后面会提到)
-
手写一下简单的
debounce函数实现// 简单的`debounce`函数实现 var debounce = function(func, wait){ var timer // 定时器 // 返回包装过的debounce函数 return function(...args){ // 如果有触发,则取消之前的触发,以当前触发为准,重新计时 if(timer){ clearTimeout(timer) } // 设置定时器 timer = setTimeout(function(){ // 定时器的回调函数:清除本次定时器,并执行函数 clearTimeout(timer) timer = null func.apply(null, args) }, wait) } }
使用
debounce的例子
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>debounce sample</title>
</head>
<body>
<div style="height: 1500px;"></div>
<script>
// debounce-definition
// scroll in debounce
var debounceFnc = debounce(function(){
console.log('scroll in debounce')
}, 1500)
window.addEventListener('scroll', debounceFnc)
</script>
</body>
</html>
打开上面例子页面的控制台,可以看到,当滚动鼠标滚动轮(或拖动滚动条),
1500毫秒内,不管滚动多少次,都会在停止并经过1500毫秒后,只执行一次。
throttle
-
原理
达到设定的时间间隔才可以触发,控制调用的频率。
用一个形象的例子来说明,
就像是游戏中放大后的冷却,放一个大之后,
大招就不可用了,需要等待冷却时间过后才可以再发一次。 -
手写一个简单的
throttle函数实现// 简单的`throttle`函数实现 var throttle = function(func, wait){ var lastTime = 0 // 用来记录上次执行的时刻 // 返回包装过的throttle函数 return function(...args){ var now = Date.now() var coolingDown = now - lastTime < wait // ↑ 距离上次执行的间隔,小于设定的间隔时间 => 则处于冷却时间 // 冷却时间,禁止放大招 if(coolingDown){ return } // 记录本次执行的时刻 lastTime = Date.now() // 冷却好了就要放大招 func.apply(null, args) } }
使用
throttle的例子
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>throttle sample</title>
</head>
<body>
<div style="height: 1500px;"></div>
<script>
// throttle-definition
// scroll in throttle
var throttleFnc = throttle(function(){
console.log('scroll in throttle')
}, 1500)
window.addEventListener('scroll', throttle)
</script>
</body>
</html>
打开上面例子页面的控制台,可以看到,当滚动鼠标滚动轮(或拖动滚动条),
不管滚动地多快甚至一直滚动不停,每次执行(打印log信息)都会间隔1500毫秒。
小小总结
对比上面的debounce防抖与throttle节流,
- 相同
debounce防抖与throttle节流都实现了单位时间内,函数只执行一次
- 不同
debounce防抖:
单位时间内,忽略前面的,响应最新的,并在延迟wait毫秒后执行throttle节流:
响应第一次的,单位时间内,不再响应,直到wait毫秒后才再响应
咱之前没做过前端(实际上是就没怎么接触过
web),
就没听说过underscore这个工具库,直到在网易云课堂·微专业
学习前端高级开发工程师的课程,才认识到了underscore。
(广告过后,)咱们来看看underscore中可配置的throttle节流与debounce防抖。
2.JavaScript工具箱underscore中的throttle(节流)与debounce(防抖)
看了上面简单实现的代码样例,让我们再来了解下underscore中的节流与防抖
引入
underscore.js文件或cdn地址:
<script src="https://cdn.bootcss.com/underscore.js/1.9.1/underscore-min.js"></script>
1) throttle 节流
_.throttle(function, wait, [options])
| 参数列表 | |
|---|---|
| function | 处理函数 |
| wait | 指定的毫秒数间隔 |
| options | 配置 |
可选。默认:{ leading: false, trailing: false } |
针对第一次触发,
leading : true 相当于先执行,再等待wait毫秒之后才可再次触发
trailing : true 相当于先等待wait毫秒,后执行
默认:
leading : false => 阻止第一次触发时立即执行,等待wait毫秒才可触发
trailing : false => 阻止第一次触发时的延迟执行,经过延迟的wait毫秒之后才可触发
可能的配置方式:
(区别在首次执行和先执行还是先等待)
| 配置 | 结果 |
|---|---|
{ leading: false, trailing: false } |
第一次触发不执行,后面同普通throttle,执行 + 间隔wait毫秒 |
{ leading: true, trailing: false } |
第一次触发立即执行,后面同普通throttle,执行 + 间隔wait毫秒 |
{ leading: false, trailing: true } |
每次触发延迟执行,每次执行间隔wait毫秒 |
{ leading: true, trailing: true } |
每一次有效触发都会执行两次,先立即执行一次,后延时wait毫秒执行一次 |
知道了原理,我们来简单写一下代码实现。
_.now = Date.now
_.throttle = function(func, wait, options){
var lastTime = 0
var timeOut = null
var result
if(!options){
options = { leading: false, trailing: false }
}
return function(...args){ // 节流函数
var now = _.now()
// 首次执行看是否配置了 leading = false = 默认,阻止立即执行
if(!lastTime && options.leading === false){
lastTime = now
}
// 配置了 leading = true 时,初始值 lastTime = 0,即可以立即执行
var remaining = lastTime + wait - now
// > 0 即间隔内
// < 0 即超出间隔时间
// 超出间隔时间,或首次的立即执行
if(remaining <= 0){ // trailing=false
if(timeOut){
// 如果不是首次执行的情况,需要清空定时器
clearTimeout(timeOut)
timeOut = null
}
lastTime = now // #
result = func.apply(null, args)
}
else if(!timeOut && options.trailing !== false){ // leading
// 没超出间隔时间,但配置了 leading=fasle 阻止了立即执行,
// 即需要执行一次却还未执行,等待中,且配置了 trailing=true
// 那就要在剩余等待毫秒时间后触发
timeOut = setTimeout(()=>{
lastTime = options.leading === false ? 0 : _.now() // # !lastTime 的判断中需要此处重置为0
timeOut = null
result = func.apply(null, args)
}, remaining);
}
return result
}
}
2) debounce 防抖
_.debounce(func, wait, [immediate])
| function | 处理函数 |
| wait | 指定的毫秒数间隔 |
| immediate | 立即执行Flag |
可选。默认:false |
功能解析:
前两个参数与前面介绍的throttle是一样的,第三个参数,
immediate指定第一次触发或没有等待中的时候可以立即执行。
知道了原理,我们来简单写一下代码实现。
// debounce 防抖:
// 用户停止输入&wait毫秒后,响应,
// 或 immediate = true 时,没有等待的回调的话立即执行
// 立即执行并不影响去设定时器延迟执行
_.debounce = function(func, wait, immediate){
var timer, result
var later = function(...args){
clearTimeout(timer)
timer = null
result = func.apply(null, args)
}
return function(...args){
// 因为防抖是响应最新一次操作,所以清空之前的定时器
if(timer) clearTimeout(timer)
// 如果配置了 immediate = true
if(immediate){
// 没有定时函数等待执行才可以立即执行
var callNow = !timer
// 是否立即执行,并不影响设定时器的延迟执行
timer = setTimeout(later, wait, ...args)
if(callNow){
result = func.apply(null, args)
}
}
else{
timer = setTimeout(later, wait, ...args)
}
return result
}
}
代码添加了注释作为说明内容,应该很容易理解。
看
underscore.js最新源码(2019-08-22当前:1.9.1)的话会发现,
除了上文介绍的配置,还加入了可取消功能(cancel)(from 1.9.0)
3.重新审视throttle(节流)与debounce(防抖)
underscore.js中通过增加可配置项来实现精细控制以应对使用者的不同需求。
其实我们一般的需求,应该是这两种基础功能结合在一起应用。
再回到最初我们自己写的极简示例demo函数上,
在没有任何配置的基本的实现里,debounce与throttle的区别在于,
当鼠标一直在滚动,debounce会一直等待结束后wait毫秒再执行,
而throttle会每间隔wait毫秒就执行一次。
再比如还是以用户输入为例,极端的例子,一直输入没有停的情况下,
输入了十分钟,结果页面在这十分钟内是没有反应的,
停止输入并经过wait毫秒之后,才会有响应,针对这种极端情况,
就用到了debounce与throttle组合。
一直输入的过程中,按照debounce是不会触发响应的,但超过了节流阀
throttle设定的wait,那至少会执行一次。
这样,就完善了用户输入的体验。
其它交互同理,一般将两者结合使用。
Talk is cheep, show you the code.
// --------------------------------------------------
function combineDebounceThrottle(func, wait){
var lastTime = 0
var timeoutD
var timeoutT
var later = function(...args){
clearTimeout(timeoutD)
clearTimeout(timeoutT)
timeoutD = null
timeoutT = null
lastTime = Date.now()
func.apply(null, args)
}
return function(...args){
var now = Date.now()
var coolingDown = now - lastTime < wait
clearTimeout(timeoutD)
if(!timeoutT && !coolingDown){
timeoutT = setTimeout(later, wait, 'throttle',...args)
}
else{
timeoutD = setTimeout(later, wait, 'debounce',...args)
}
}
}
// --------------------------------------------------
var func = combineDebounceThrottle(function(logFlag){
console.log('scrolling in throttle-debounce-combined')
}, 1500)
window.addEventListener('scroll', func)
// --------------------------------------------------
啰嗦两句,
只用 debounce 的话,一直滚动就会一直等待,
加入了 throttle,则超过 wait 毫秒就会执行一次,throttle 延迟执行等待中的时候,
仍然有输入则在结束后,还会触发 debounce 的延时回调。