函数节流与防抖

前言

博客中的大部分文章 大部分都有一个引子,要说明的通常是一些零零碎碎或者大多耳熟能详的 “知识点”

我认为遇到了问题 解决了问题 然后才会形成一个知识点而不是为了应用某个知识点去搭建场景,否则 真的是过目即忘
解决最近一个问题之前 并没有计划写关于防抖和节流的问题,问题引导你说 需要重视这一块儿了

此文系 不要再问我 XX 系列之 不要再问我函数节流和防抖了

引出问题

需求:记录用户的浏览深度

---

一旦你使用了类似 scroll 的浏览器事件,触发频率比较高,若稍处理函数微复杂,需要较多的运算执行时间,响应速度跟不上触发频率,往往会出现延迟,会出现卡顿甚至引发假死

在资源有限的情况下 我们可以想办法只响应部分请求,事实上某些场景下的密集性请求,并不是我们需要的

此时 我并不知道 需要用 “防抖” 这个晦涩的结论去解决,只知道目的是只是在浏览到这辆车的时候打点


那程序中要怎么体现我浏览到这个概念呢?

先要约定一件事
假设用户浏览到某一辆车之后停顿了超过你预设的时间值 就认为是浏览了

利用 setTimeout 实现

1
2
3
4
5
6
window.addEventListener('scroll', function(){
var timeout = null;
return function() {
clearTimeout(timeout);
timeout = setTimeout(fn,200);
}, false);

通过闭包保存一个定时器的标记 timeout,再次执行的时候 clear 掉之前的,又重新计时 这就保证了 触发非常频繁的 scroll 事件合并成一次执行。当调用动作过 n 毫秒后,才会执行该动作,若在这 n 毫秒内又调用此动作则将重新计算执行时间 所以短时间内的连续动作永远只会触发一次,比如说用手指一直按住一个弹簧,它将不会弹起直到你松手为止

现在的效果

fangdou111

好像受控制了 滑动过程中不会执行,一旦我们停下超过 n 毫秒,会执行一次
由此得到一个结论
处理此类问题就是要保证函数在特定的时间内(你设置的延迟时间)不被再调用后执行

我们的问题解决了,上网随便一搜,它是一类知识点,越发感兴趣惹


得出结论 引出 防抖 与 节流

所以说我们直接接触到了 函数防抖,在此之前 我觉得它是晦涩难懂的,现在越发清晰了
说到这儿 不得不提另一个兄弟 函数节流,因为应用场景相似而不相同,所以经常会被拿来比较

概念

网上有很多关于两者的概念
简单来讲:
函数节流:指定时间间隔内只会执行一次任务

1
2
3
4
🌰1
一个比较形象的例子是如果将水龙头拧紧直到水是以水滴的形式流出,那你会发现每隔一段时间,就会有一滴水流出
🌰2
地铁闸机,每个人进入后3秒后门关闭,等待下一个人进入。

函数防抖:任务频繁触发的情况下,只有任务触发的间隔超过指定间隔的时候,任务才会执行 (当一个动作连续触发,只执行最后一次)

1
2
🌰
用手指一直按住一个弹簧,它将不会弹起直到你松手为止

看下图 感受一下三种环境对于 mousemove 事件回调的执行情况
fn_dou

竖线的疏密程度代表事件之行的频繁程度
可以得到

  • 正常情况下 函数执行的非常频繁
  • 去抖之后很稀疏 只有当鼠标停止移动时才会执行一次
  • 节流分布的比较均匀 每隔一段时间就会执行一次

demo 示例

原生 scroll

scrolldemo

函数去抖

debouce

函数节流
throllte

我们在真实场景中可以感受到无论采取哪种方案都会明显减少了回调的执行,得到了 都是用来控制某个函数在一定时间内执行次数的多少以优化高频率执行 js 代码的一种技巧,两者相似而又不同的结论

那么 我们 如何选择这两种方案?

优化方案的应用场景

选择哪个 取决于应用场景


函数防抖

如果你的需求是连续的时间只需要触发一次回调

比如:

  1. 搜索框输入 可能需要等到用户最后一次输入完 再去发送请求
  2. 手机号,邮箱等输入检测
  3. 浏览器窗口的 resize 你肯定要等到窗口调整完成后再进行渲染
  4. scroll 事件等

函数节流

固定时间间隔执行的

比如:

  1. 滚动加载 常常需要滚动到底部加载下一页
  2. 表单的重复提交
  3. 进度条的更新
  4. 高频的点击(比如抽奖)
  5. 高频的鼠标移动,游戏射击类的

哦 理解了如何选择 下一个问题就是如何用


实现原理

引出问题模块 其实我们已经做到了 防抖

函数防抖(debounce)简单实现

1
2
3
4
5
6
7
8
9
10
11
window.addEventListener('scroll', this.debounce(this.scrollListener, 500), false);

debounce = (fn, wait) => {
var timeout = null;
return function() {
if(timeout !== null){
clearTimeout(timeout);
};
timeout = setTimeout(fn, wait);
}
}

函数防抖在第一次执行时,有 500ms 的延迟。再次执行时,若前一个定时任务未执行完,则 clear 掉定时任务,重新定时


函数节流(throttle)

定时器版本实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const _.throttle = (func, wait) => {
let timer;

return () => {
if (timer) {
// 判断是否已空闲,如果在执行中,则直接return
return;
}

timer = setTimeout(() => {
func();
timer = null;
}, wait);
};
};

函数节流的目的,是为了限制函数一段时间内只能执行一次 使用 setTimeout 执行。在延时的时间内,方法若被触发,则直接退出方法。从而达到函数一段时间内只执行一次的目的

时间戳版简单实现

1
2
3
4
5
6
7
8
9
10
const throttle = (func, wait) => {
let last = 0;
return () => {
const current_time = +new Date();
if (current_time - last > wait) {
func.apply(this, arguments);
last = +new Date();
}
};
};

其实现原理,通过比对上一次执行时间与本次执行时间的时间差与间隔时间的大小关系,来判断是否执行函数。若时间差大于间隔时间,则立刻执行一次函数。并更新上一次执行时间。


函数节流与函数防抖异同

其实到这里应该比较能清晰的理解甚至选择适合真实场景的优化方案了

相同点

  • 都可以通过延时器实现
  • 目的都是 降低回调执行频率 节约计算机资源

不同点

  • 其实从两者的概念也能看出来

  • 函数防抖:关注一定时间连续触发,只在最后执行一次

  • 函数节流:一段时间内只执行一次。

写在最后的话

很多时候 带着问题来验证结论 更能把自己置身到场景中考虑

参考文章

https://jinlong.github.io/2016/04/24/Debouncing-and-Throttling-Explained-Through-Examples/
https://github.com/hanzichi/underscore-analysis/issues/20
https://segmentfault.com/a/1190000008768202