函数的节流和防抖
什么是节流和防抖
我觉得描述节流和防抖最直观的方式是举个实际的例子。
节流
假设网页中有一个下载按钮,用户点击后会开始下载指定的资源。
但是现实中有一种情况——用户可能会连续、快速地点击多次(可能因为有延迟所以暂时没反应),如果我们不进行处理,就会连续下载很多次,这是一种不好的用户体验。
这时有一种比较好的处理方案:用户的第一次点击是生效的,然后在一定时间内(假设为1秒),不管用户点击多少次,都不会生效。1秒后,如果用户又点击了一次,我们就再生效一次。
这样,每一秒内最多只能有一次点击真正生效,也就是所谓的“节流”
防抖
假设我们在网页中想实现一个功能:
当用户输入文本时,我们自动向后台发送请求,将用户输入的文本保存起来。
如果不进行任何处理,用户每进行一次输入就会发送一次请求,这样就会导致请求发送的太多。有没有什么解决方案呢?
肯定是有的,那就是当用户输入开始后,只有当用户在一定的时间间隔内没有输入,我们才发送请求。如果用户持续输入,我们就不发送。(在这种场景下这种方案是很好的,因为用户输入总会有停顿和停止的时候)
还有一些情况下防抖更有用:比如我们想监听网页滚动的事件,如果监听到就调用一个函数,如果不作任何处理,网页可能每滚动一个像素都要调用函数,我们完全可以设置当滚动发生后,如果滚动停止了一小段时间,我们再调用函数。
如何实现节流函数
节流函数的实现思路是:
-
调用节流后的函数
-
检查该函数之前是否调用过(通过设置一个时间戳)
-
如果该函数从来没调用过:
- 执行该函数
- 设置一个时间戳
-
如果该函数调用过:
检查现在距离上次调用时间过去了多久:
如果已经超过了我们限制的某个时长,就执行该函数,再设置一个时间戳
如果没有超过,就什么事情都不作
-
在开始代码之前,我们先来快速了解一下JavaScript中的闭包的特点,对于下面这一段代码:
如果你对闭包没有了解,请先去看看javascript闭包
function fn(b) { //这个函数会返回一个函数
var a = 1;
return function () { //这个返回的函数是一个闭包
a++;
console.log(a);
}
}
let func = fn(); //获取一个闭包函数
//下面开始调用这个闭包函数
console.log("第1次调用")
func();
console.log("第2次调用")
func();
console.log("第3次调用")
func();
console.log("第4次调用")
func();
得到的是这样的输出结果
第1次调用
2
第2次调用
3
第3次调用
4
第4次调用
5
注意这个输出结果说明了:
一个闭包函数(即func
)不管被调用多少次,都共享同样的闭包资源(即fn
中的a
变量)。
下面再来看具体的实现:
const throttle = function (fn, limit) {
//这个throttle函数会返回一个函数,我们不妨将返回的函数称作返回函数,即闭包函数
let prevTime = null; //返回函数上一次被调用的时间
//返回一个函数,这个函数的作用是:
//如果满足一定的条件,就设置一个定时器,定时器中执行fn
return () => {
//这个函数只有在throttle函数被调用的时候才会执行
//获取这个函数被调用时的作用域和参数,(存下来传递给fn)
let context = this;
let args = arguments;
if (!prevTime) {
//如果prevTime为null,说明返回函数被第一次调用
//第一次调用我们直接执行fn
fn.apply(context, args);
//记录当前这次执行fn的时间
prevTime = Date.now();
} else {
//如果之前返回函数已经被调用过了,则看看距离上次调用是否过去足够长的间隔了。
//现在我们要获取本次调用距离上次调用的时间间隔
let hasPass = Date.now() - prevTime;
if (hasPass >= limit) { //如果时间间隔大于limit,则直接调用fn
fn.apply(context, args);
prevTime = Date.now();
}
//否则什么都不做
} //end of else => (!prevTime)
}; //end of return
}
如何实现防抖函数
如果你忘了什么是防抖函数,可以先去本文开始的地方看看。
防抖函数的实现思路是:
- 调用防抖后的函数
- 清除之前设置的定时器(如果有的话)
- 设置一个新的定时器,定时器的作用是在一定的时间后执行我们想执行的函数。(并存下来这个定时器)
实现思路很简单,下面是代码:
const debounce = function (fn, delay) {
//这里的逻辑是,每当返回函数被调用时,我们就清除之前设置的timer,重新设置一个新的timer
let prevTimer = null;
return () => {
clearTimeout(prevTimer);
let context = this;
let args = arguments;
prevTimer = setTimeout(() => {
fn.apply(context, args);
}, delay);
}
}