前言
在 JavaScript 开发中,防抖是一种常用的技术手段,它能有效地优化性能和提升用户体验。
防抖的核心思想: 在短时间内连续触发事件时,只执行最后一次或几次事件处理函数,而不是立即响应每一次触发。
举个例子: 想象一下,当用户频繁操作时,如快速滚动、连续输入等,如果每次操作都立即执行相应的函数,可能会导致不必要的计算和资源浪费。这时,防抖就派上用场了。
防抖的关键: 在于设置一个延迟时间。在触发事件后,启动一个定时器,如果在定时器到期之前再次触发事件,就会重置定时器,只有当定时器到期且没有新的触发时,才会真正执行事件处理函数。
防抖的部分用途举例: 防抖在实际应用中有广泛的用途,比如搜索框实时搜索的优化、按钮连续点击的处理等。
手写防抖案例
这里还是直接步入正题,手写防抖,写完之后大家应该就能够更好地理解什么是防抖
,防抖有什么作用
,怎么手写防抖
。
js
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>debounce</title>
</head>
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn');
function handle() {
// ajax请求
console.log('提交');
}
btn.addEventListener('click', handle);
</script>
</body>
</html>
- 首先大家来看看这份代码,很简单,就是一个button按钮,然后为这个按钮绑定了一个监听器,当点击这个button按钮时,就会触发这个监听器调用这个回调函数,这个回调函数直接封装在外面了,函数内容就是打印一个提交。
- 那么大家思考一下,如果用户一直点击这个按钮,控制台是不是就会一直输出提交?试想一下,如果是一个大型项目,有很多人同时在线这么干,咱们的服务器岂不是一下就崩掉了?
- 接下来我们就带着这个问题来思考一下,如果解决?(防抖来了。)
js
// 防抖函数
function debounce(fn) {
return function() {
setTimeout(()=>{
fn();
}, 1000);
}
}
既然我们要做防抖,那我们不妨就直接写一个防抖函数,这个函数有一个返回值,返回值是一个函数,在此我们姑且称之为子函数。
- 那么为什么需要这个子函数呢?
- 我们刚刚是给button按钮绑定了一个监听器,这个监听器是需要一个回调函数的,如果我们这里没有返回一个函数,而是直接把这个防抖函数的调用丢进去,那就会直接报错了。
js
btn.addEventListener('click', debounce(handle));
现在我们继续思考,是不是点一下button按钮,这个防抖函数就会被调用一次,调用一次就会返回一个子函数,然后子函数被调用,子函数就会在一秒之后调用fn函数,fn函数就是打印提交。那还是没解决问题,他只是延迟了一秒钟,但是服务器该响应我们的提交还是要相应提交。
因此接下来我们这么干,声明一个timer,首先他是空的,然后传给子函数里面,在子函数里面我们做删除定时器的操作,只要把定时器删掉了,这个fn函数就不会被调用了
js
function debounce(fn) {
let timer = null;
return function() {
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(()=>{
fn();
}, 1000);
}
}
看看目前的代码,现在的情况是不是,我们用户如果一秒钟都还没到,就又点了一下提交,那子函数就会删除这个定时器一秒钟之后才去执行这个事情,那么在1秒钟之内,如果你疯狂发送请求,服务器也不需要搭理你了。
思考 (闭包)
这个地方是怎么做到的?运用了什么原理? 回想一下之前写的文章,这里其实就是一个函数,里面又有一个函数,但是这个里面的子函数,父级函数管不到它了,它的调用完全是归于外界管理,你要说父函数执行完毕没有,他确实是执行完毕了,既然执行完毕了就应该把它的执行上下文对象给销毁掉,但是js引擎考虑到它的子函数还要用timer这个变量,如果直接把执行上下文销毁掉了,这个子函数就无法访问到这个变量了,于是他就留下了一个小背包,这个小背包就是所谓的闭包
。
至此,从表面上看,我们要实现的防抖它的确是实现了,可是,这就完成了防抖吗?
js
<script>
let btn = document.getElementById('btn');
function handle() {
// ajax请求
console.log('提交', this);
}
btn.addEventListener('click', handle);// this 指向btn
// 防抖函数
function debounce(fn) {
let timer = null;
return function() {
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(function() {
fn();
}, 1000);
}
}
</script>
大家来看看这份代码,在我们没有给程序做防抖之前,这里的this指向谁?我们做完防抖之后,会不会改变这个this的指向
?
- 在这份代码中,这里的this指向btn,看this指向哪,我之前有一篇文章讲过,这里要看这个函数是什么绑定方式,这里的handle函数是由监听器绑定的,由监听器触发调用,因此它属于this中的隐式绑定,因此这里的this应该指向btn
- 但是在我们设置防抖之后,现在可以回去看看刚刚做了防抖的代码,那里的this事实上是指向window的,handle函数的调用是在定时器内的回调函数中调用的,定时器的回调函数指向window,因此在我们做了防抖以后这个this会指向window。
- 既然如此,我们虽然帮助别人把防抖搞定了,相当于搞定了一个大问题,但是带了了一堆的小问题,在大项目中,更改了这些this指向...等等问题很有可能会带来很多的bug。
- 因此我们如何把这个this指向给他掰回去?其实之前写的this文章中也有讲到如何掰弯this指向(显式绑定的方式)call、apply、bind三种方法
js
// 防抖函数
function debounce(fn) {
let timer = null;
return function(e) {
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(()=>{
fn.call(this);
}, 1000);
}
}
- 在这里我们使用了箭头函数,而箭头函数是没有this这个机制的,它里面的fn中的this会去找它的外层非箭头函数的(子函数)的this,而这个子函数又是由btn调用的,通过这样一个操作,这个this的指向就成功被掰回去了。
- 这里大家有没有发现,子函数中还传递了一个参数e
js
<script>
let btn = document.getElementById('btn');
function handle(e) {
// ajax请求
console.log('提交', this, e);
}
btn.addEventListener('click', handle);// this 指向btn
// 防抖函数
function debounce(fn) {
let timer = null;
return function(e) {
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(()=>{
fn.call(this, e);
}, 1000);
}
}
</script>
大家看看在我们没做防抖的时候,这个函数的默认参数e,里面是什么
但是在我们设置了防抖以后,这个函数的默认参数e,就变成了undefined。
- 为什么变成了undefined
- 怎么把这个bug给它修正
js
// 防抖函数
function debounce(fn) {
let timer = null;
return function(e) {
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(()=>{
fn.call(this, e);
}, 1000);
}
}
- 通过传参,把函数上的e给他传回去
- 回头想一下,我们掰弯this的时候,是不是把this给传进去,this这个关键字在这里传递,大家会不会看着很别扭?因此在这里我们可以自己设置一个变量例如that,把that给传进去
js
// 防抖函数
function debounce(fn) {
let timer = null;
return function(e) {
const that = this;
// 如果第二次的时间没到1s,就销毁上一次的定时器
clearTimeout(timer);
timer = setTimeout(function() {
fn.call(that, e);
}, 1000);
}
}
如果改成这样,是不是看上去就舒服很多了。
小结
- 防抖是一种优化性能和提升用户体验的技术手段,其核心思想是在短时间内连续触发事件时,只执行最后一次或几次事件处理函数。
- 防抖的关键在于设置一个延迟时间,在触发事件后启动一个定时器,如果在定时器到期之前再次触发事件,就会重置定时器,只有当定时器到期且没有新的触发时,才会真正执行事件处理函数。
- 防抖在实际应用中有广泛的用途,如搜索框实时搜索的优化、按钮连续点击的处理等。
- 通过一个简单的示例演示了如何手写防抖函数,并详细介绍了防抖函数的实现过程,包括如何设置延迟时间、如何处理连续触发事件、如何避免 this 指向问题以及如何解决参数传递问题。