一、什么是防抖
防抖(Debounce)是一种用于减少特定事件触发频率的技术。在编程中,它通常用于确保函数或方法不会在很短的时间内被频繁调用,这有助于优化性能并避免不必要的计算或操作。
防抖的实现原理是,在事件被触发后,一个定时器会被设置。如果在定时器完成之前,相同的事件再次被触发,那么原来的定时器会被取消,并重新设置一个新的定时器。这样,只有在最后一次事件触发后的一定时间内没有再次触发,定时器才会执行其回调函数。
应用场景:
- 登录与发送短信:在连续点击登录按钮或发送短信时,防抖技术能够确保不会因用户点击过快而发送多次请求。
- 表单验证:在用户输入表单信息时,防抖技术可以确保不会因为频繁触发验证逻辑而导致性能降低。
- 实时搜索与保存:在文本编辑器或搜索框中实现实时搜索和保存功能时,防抖技术可以确保在用户停止输入一段时间后执行搜索或保存操作,避免用户连续输入导致的频繁触发。
- 窗口大小调整:在调整浏览器窗口大小时,resize事件可能会被频繁触发,防抖技术可以确保只执行一次操作,避免不必要的计算。
- 鼠标移动事件:实现一些需要用户停止移动鼠标后再执行的功能时,如拖拽功能,防抖技术可以减少事件的处理频率。
二、前置准备
-
准备一个html文件和一个debounce.js文件,debounce.js文件用来编写防抖函数
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <script src="./debounce.js"></script> </body> </html>
// debounce.js const debounce = () => {};
-
给
div
绑定点击事件<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <script src="./debounce.js"></script> <script> const clickEvent = (e) => { console.log("点击事件触发", e, this) } document.querySelector(".debounce").addEventListener("click", clickEvent) </script> </body> </html>
进行点击测试,发现目前this指向的是Window,因为箭头函数没有this,通过作用域链往外找,就找到Window了。
将箭头函数改为普通函数,就能将this指向改为div。但是这里暂时先不改,后面遇到问题再说。
-
将clickEvent方法传递给debounce进行处理
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <script src="./debounce.js"></script> <script> const clickEvent = (e) => { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000) document.querySelector(".debounce").addEventListener("click", debounceClickEvent) </script> </body> </html>
现在再进行点击测试,发现控制台什么也没有输出。因为目前debounce方法还没写方法体,没有返回值,所以
debounceClickEvent
是undefined,所以什么也不会触发。下面我们就一步步写防抖函数
三、基础防抖实现
-
思考防抖函数需要接收什么参数,又需要返回什么
- 接收参数:一个点击事件的处理方法、延迟执行的时间
- 返回值:做了防抖处理的方法
根据以上条件,可以先写出如下代码
// debounce.js const debounce = (fun, time) => { return fun; };
上面代码接收了一个方法,又直接将该方法返回了。等于什么也没做,至少也得做个延时处理吧
// debounce.js const debounce = (fun, time) => { return () => { setTimeout(fun, time); }; };
上面的方法虽然做了延时处理,但还是造成了处理方法被多次调用
那我们应该怎样让前面的处理方法被取消执行呢?既然处理方法都放进了定时器,那我们就把定时器清除就行了。
// debounce.js const debounce = (fun, time) => { let timer; return () => { timer && clearTimeout(timer); timer = setTimeout(fun, time); }; };
这下多次点击,就只触发了最后一次处理方法了。但是通过打印可以发现,this指向的是Window,事件源也是undefined。
我们如何将this指向变成调用方法的
div
,又如何获取事件源呢 -
改变this指向
我们先分析一下,为什么this指向的是window?
const clickEvent = (e) => { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000) // debounce.js const debounce = (fun, time) => { let timer; return () => { timer && clearTimeout(timer); timer = setTimeout(fun, time); }; };
上面的写法,其实等价于下面的写法
// debounce.js const debounce = (fun, time) => { let timer; return () => { timer && clearTimeout(timer); timer = setTimeout((e) => { console.log("点击事件触发", e, this) }, time); }; };
那再来分析一下this指向,箭头函数没有this,它通过作用域链往外找,就找到Window了
那我们在哪里能获取到正确的this指向呢?来看看哪个方法是被div调用的,是不是return 后面那个方法
既然这个方法是div调用的,那我们应该可以拿到this,但是这里也是箭头函数,没有this。所以我们先将它修改为普通函数
// debounce.js const debounce = (fun, time) => { let timer; return function () { console.log(this); timer && clearTimeout(timer); timer = setTimeout(fun, time); }; };
我们打印一下this,看看是不是div
这里的this指向确实是指向div的,那我们就可以通过call、apply、bind等方法修改fun的this指向了
// debounce.js const debounce = (fun, time) => { let timer; return function () { timer && clearTimeout(timer); timer = setTimeout(fun.bind(this), time); }; };
再来看看能打印出正确的this吗
结果this还是指向的Window,别忘了fun是一个箭头函数,它没有this,又怎么能去修改呢。
所以我们需要将这个fun(clickEvent)也改为普通函数
const clickEvent = function (e) { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000) document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
这下再来看看this指向正确了吗
-
获取事件源
为什么这里的e打印出来是undefined?
我们来看看div调用的是哪个方法,是不是debounce函数中return的那个方法。那这个方法应该是可以接收到事件源的。
再看看fun,我们使用bind的时候,根本就没有给它设置参数,所以e打印出来是undefined
// debounce.js const debounce = (fun, time) => { let timer; return function (e) { console.log(e); timer && clearTimeout(timer); timer = setTimeout(fun.bind(this), time); }; };
从返回的方法里面确实可以拿到事件源,那我们将这个 e 传递给bind 的第二个参数就好了
// debounce.js const debounce = (fun, time) => { let timer; return function (e) { timer && clearTimeout(timer); timer = setTimeout(fun.bind(this, e), time); }; };
四、接收多个参数
-
上面已经实现了最基本的防抖函数,但还有一些地方需要优化
- 如果传递了多个参数又如何接收
- 每次触发新的点击事件,会清空定时器。那最后一次执行完了,又怎么清空呢?这个对象始终没有释放掉
-
接收多个参数
// debounce.js const debounce = (fun, time) => { let timer; return function (...args) { timer && clearTimeout(timer); timer = setTimeout(fun.bind(this, ...args), time); }; };
-
最后一次执行完毕,将timer释放掉
// debounce.js const debounce = (fun, time) => { let timer; return function (...args) { timer && clearTimeout(timer); timer = setTimeout(() => { fun.apply(this, args); timer = null; }, time); }; };
五、取消处理方法
-
怎么将最后一次的处理方法给取消呢?
想办法拿到定时器timer,然后使用
clearTimeout(timer)
就可以了 -
准备一个div,作为取消按钮
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <div class="cancle">取消</div> <script src="./debounce.js"></script> <script> const clickEvent = function (e) { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000) const cancle = () => { } document.querySelector(".debounce").addEventListener("click", debounceClickEvent) document.querySelector(".cancle").addEventListener("click", cancle) </script> </body> </html>
-
在返回的函数身上再绑定一个方法用来清除定时器
// debounce.js const debounce = (fun, time) => { let timer; const debounceEvent = function debounceEvent(...args) { timer && clearTimeout(timer); timer = setTimeout(() => { fun.apply(this, args); timer = null; }, time); }; debounceEvent.cancle = () => { timer && clearTimeout(timer); timer = null; }; return debounceEvent; };
-
给取消按钮绑定方法
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <div class="cancle">取消</div> <script src="./debounce.js"></script> <script> const clickEvent = function (e) { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000) const cancle = () => { debounceClickEvent.cancle() } document.querySelector(".debounce").addEventListener("click", debounceClickEvent) document.querySelector(".cancle").addEventListener("click", cancle) </script> </body> </html>
六、立即执行
-
如何让第一次处理函数立即执行,后面再做防抖处理
可以通过一个变量来控制,第一次立即执行,然后将该变量取反,后面执行的时候,使用原来的逻辑
-
为debounce函数添加参数,用来标志是否第一次立即执行
// debounce.js const debounce = (fun, time, immediately = false) => { let timer; const debounceEvent = function debounceEvent(...args) { timer && clearTimeout(timer); timer = setTimeout(() => { fun.apply(this, args); timer = null; }, time); }; debounceEvent.cancle = () => { console.log("清除定时器"); timer && clearTimeout(timer); timer = null; }; return debounceEvent; };
-
修改第一次执行的逻辑
// debounce.js const debounce = (fun, time, immediately = false) => { let timer; // 是否已经立即执行 let running = false; const debounceEvent = function debounceEvent(...args) { timer && clearTimeout(timer); // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行 if (immediately && !running) { // 第一次已经立即执行了,后面再次触发就不能再立即执行了 running = true; fun.apply(this, args); } else { timer = setTimeout(() => { fun.apply(this, args); timer = null; // 最后一次防抖方法完成后,下一次还可以立即执行 running = false; }, time); } }; debounceEvent.cancle = () => { console.log("清除定时器"); timer && clearTimeout(timer); timer = null; // 取消最后一次防抖方法后,恢复下一次的立即执行 running = false; }; return debounceEvent; };
上面的代码实现了第一次触发时立即执行,然后每次触发做防抖处理。最后一次防抖方法处理完成(或被取消)后,在下一次触发时,又可以立即执行。
但是这仍然存在一个小问题,如果第一次立即执行后不触发频繁的点击操作,而是等第一次完成之后,再点击,这时还会立即执行吗?很明显不能。目前第一次立即执行后,想要恢复立即执行,就必须经过频繁触发事件,让最后一次防抖方法被处理了,才能再次恢复立即执行。
在下面的代码中,我们使用了一个定时器。在第一次立即执行完成后,开启一个定时器,在一段时间后恢复立即执行,使再次点击时,可以立即执行。但是,如果在这一段时间内,频繁的触发了点击事件,那就清除定时器,在最后一次防抖处理方法完成后,再恢复立即执行。
// debounce.js const debounce = (fun, time, immediately = false) => { let timer; // 是否已经立即执行 let running = false; // 恢复立即执行的定时器 let timerRunning; const debounceEvent = function debounceEvent(...args) { timer && clearTimeout(timer); // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行 if (immediately && !running) { // 第一次已经立即执行了,后面再次触发就不能再立即执行了 running = true; fun.apply(this, args); // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行 timerRunning = setTimeout(() => { running = false; }, time); } else { // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行 timerRunning && clearTimeout(timerRunning); timer = setTimeout(() => { fun.apply(this, args); timer = null; // 最后一次防抖方法完成后,下一次还可以立即执行 running = false; }, time); } }; debounceEvent.cancle = () => { console.log("清除定时器"); timer && clearTimeout(timer); timer = null; // 取消最后一次防抖方法后,恢复下一次的立即执行 running = false; }; return debounceEvent; };
七、获取防抖函数的返回值
-
先来手动调用一下点击处理函数
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <div class="cancle">取消</div> <script src="./debounce.js"></script> <script> const clickEvent = function (e) { console.log("点击事件触发", e, this) } const debounceClickEvent = debounce(clickEvent, 2000, true) const cancle = () => { debounceClickEvent.cancle() } document.querySelector(".debounce").addEventListener("click", debounceClickEvent) document.querySelector(".cancle").addEventListener("click", cancle) // 手动调用点击处理函数 debounceClickEvent() debounceClickEvent() debounceClickEvent() debounceClickEvent() </script> </body> </html>
我们在上面手动调用了4次点击事件的处理函数,查看控制台也发现了打印了两次,一次是立即执行,一次是防抖处理的最后一次执行
-
给点击处理函数设置返回值
下面,我们给clickEvent方法设置一个返回值。
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <div class="cancle">取消</div> <script src="./debounce.js"></script> <script> const clickEvent = function (e) { console.log("点击事件触发", e, this) const result = "请求结果数据" return result } const debounceClickEvent = debounce(clickEvent, 2000, true) const cancle = () => { debounceClickEvent.cancle() } document.querySelector(".debounce").addEventListener("click", debounceClickEvent) document.querySelector(".cancle").addEventListener("click", cancle) // 手动调用点击处理函数 console.log(debounceClickEvent()) console.log(debounceClickEvent()) console.log(debounceClickEvent()) console.log(debounceClickEvent()) </script> </body> </html>
然后需要在
debounceEvent
方法中将结果返回// debounce.js const debounce = (fun, time, immediately = false) => { let timer; // 是否已经立即执行 let running = false; // 恢复立即执行的定时器 let timerRunning; const debounceEvent = function debounceEvent(...args) { // 返回结果 let result; timer && clearTimeout(timer); // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行 if (immediately && !running) { // 第一次已经立即执行了,后面再次触发就不能再立即执行了 running = true; // 获取方法执行的返回结果 result = fun.apply(this, args); // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行 timerRunning = setTimeout(() => { running = false; }, time); } else { // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行 timerRunning && clearTimeout(timerRunning); timer = setTimeout(() => { // 获取方法执行的返回结果 result = fun.apply(this, args); timer = null; // 最后一次防抖方法完成后,下一次还可以立即执行 running = false; }, time); } // 返回结果 return result; }; debounceEvent.cancle = () => { console.log("清除定时器"); timer && clearTimeout(timer); timer = null; // 取消最后一次防抖方法后,恢复下一次的立即执行 running = false; }; return debounceEvent; };
查看控制输出
第一次是立即执行的,所以可以拿到返回值。但最后一次是异步执行的,所以拿不到。可以使用Promise来处理
// debounce.js const debounce = (fun, time, immediately = false) => { let timer; // 是否已经立即执行 let running = false; // 恢复立即执行的定时器 let timerRunning; const debounceEvent = function debounceEvent(...args) { return new Promise((resolve, reject) => { // 返回结果 let result; timer && clearTimeout(timer); // 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行 if (immediately && !running) { // 第一次已经立即执行了,后面再次触发就不能再立即执行了 running = true; result = fun.apply(this, args); resolve(result); // 第一次立即执行已经完成了,我们在一段时间后恢复立即执行 timerRunning = setTimeout(() => { running = false; }, time); } else { // 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行 timerRunning && clearTimeout(timerRunning); timer = setTimeout(() => { result = fun.apply(this, args); resolve(result); timer = null; // 最后一次防抖方法完成后,下一次还可以立即执行 running = false; }, time); } }); }; debounceEvent.cancle = () => { console.log("清除定时器"); timer && clearTimeout(timer); timer = null; // 取消最后一次防抖方法后,恢复下一次的立即执行 running = false; }; return debounceEvent; };
<!-- test.html --> <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>防抖</title> </head> <body> <div class="debounce">触发防抖事件</div> <div class="cancle">取消</div> <script src="./debounce.js"></script> <script> const clickEvent = function (e) { console.log("点击事件触发", e, this) const result = "请求结果数据" return result } const debounceClickEvent = debounce(clickEvent, 2000, true) const cancle = () => { debounceClickEvent.cancle() } document.querySelector(".debounce").addEventListener("click", debounceClickEvent) document.querySelector(".cancle").addEventListener("click", cancle) // 手动调用点击处理函数 // console.log(debounceClickEvent()) // console.log(debounceClickEvent()) // console.log(debounceClickEvent()) // console.log(debounceClickEvent()) debounceClickEvent().then(res => console.log(res)) debounceClickEvent().then(res => console.log(res)) debounceClickEvent().then(res => console.log(res)) debounceClickEvent().then(res => console.log(res)) </script> </body> </html>
查看控制台输出,这下就没问题了
八、完整代码
<!-- test.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>防抖</title>
</head>
<body>
<div class="debounce">触发防抖事件</div>
<div class="cancle">取消</div>
<script src="./debounce.js"></script>
<script>
const clickEvent = function (e) {
console.log("点击事件触发", e, this)
const result = "请求结果数据"
return result
}
const debounceClickEvent = debounce(clickEvent, 2000, true)
const cancle = () => { debounceClickEvent.cancle() }
document.querySelector(".debounce").addEventListener("click", debounceClickEvent)
document.querySelector(".cancle").addEventListener("click", cancle)
// 手动调用点击处理函数
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
// console.log(debounceClickEvent())
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
debounceClickEvent().then(res => console.log(res))
</script>
</body>
</html>
// debounce.js
const debounce = (fun, time, immediately = false) => {
let timer;
// 是否已经立即执行
let running = false;
// 恢复立即执行的定时器
let timerRunning;
const debounceEvent = function debounceEvent(...args) {
return new Promise((resolve, reject) => {
// 返回结果
let result;
timer && clearTimeout(timer);
// 开启了立即执行并且还没有立即执行,则说明这是第一次,直接立即执行
if (immediately && !running) {
// 第一次已经立即执行了,后面再次触发就不能再立即执行了
running = true;
result = fun.apply(this, args);
resolve(result);
// 第一次立即执行已经完成了,我们在一段时间后恢复立即执行
timerRunning = setTimeout(() => {
running = false;
}, time);
} else {
// 如果频繁触发了事件,则不恢复立即执行,而是等最后一次处理方法完成再恢复立即执行
timerRunning && clearTimeout(timerRunning);
timer = setTimeout(() => {
result = fun.apply(this, args);
resolve(result);
timer = null;
// 最后一次防抖方法完成后,下一次还可以立即执行
running = false;
}, time);
}
});
};
debounceEvent.cancle = () => {
console.log("清除定时器");
timer && clearTimeout(timer);
timer = null;
// 取消最后一次防抖方法后,恢复下一次的立即执行
running = false;
};
return debounceEvent;
};
标签:防抖,const,debounceClickEvent,debounce,timer,js,面试,fun
From: https://www.cnblogs.com/finish/p/18078538