前言
在Web交互场景中,用户频繁触发事件(如表单提交、按钮点击)时,前端往往会产生大量重复请求。这种现象如同在服务器端敲响密集的警报铃------不仅导致带宽浪费,更可能引发服务端过载甚至雪崩效应。面对这类问题,前端工程师需要掌握两个核心技术策略:防抖(Debounce) 与节流(Throttle)。
在这个场景下,如果用户在短时间内狂点提交,就会向后端发送很多无意义的请求,会加重后端的负担。所以我们要用某种手段去限制用户在短时间内点过多请求,避免给服务器造成太大压力。
要怎么解决这个问题呢?
1.防抖 :(思想:最后一次有效) 点了一次之后开始计时,计时结束再去执行提交。如果我点了一下,但是倒计时还没结束,我又点了一下,则上一次的点击行为作废,从这一次的点击行为开始算,重新倒计时,倒计时内没有再点提交,才去执行相应代码。即在规定的时间到来之前没有二次触发行为了才去执行。(依赖定时器)
2.节流: 在规定时间内只执行一次。比如哪怕一秒钟点了10次我也最多触发一次
3.按钮禁用: 点击一次之后让按钮变成不可点击状态,等执行完后再变回可点击状态。这个方案比较简单,但是会频繁的操作dom结构
一、防抖
在规定的时间内如果没有二次触发行为,则执行,否则放弃上一次的事件行为,直接从当前行为开始重新计时。
怎么实现?
1.实现防抖效果
写一个防抖的工具函数debounce
,把点击提交要执行的函数放进debounce
函数里,第二个参数是倒计时。来看以下代码,为了符合 addEventListener
语法,debounce
函数要返回一个函数体。(具名函数debounce
内部定义了一个匿名函数,内部的匿名函数被拿到函数外部去调用了,内部函数要用到外部函数的某个变量时,就会形成闭包。)
xml
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式
function handle (){
console.log('向后端发送请求');
}
function debounce(fn,wait){ //防抖工具函数
return function(){
setTimeout(()=>{
fn()
},wait);
}
}
</script>
</body>
但写成这样还不够,匿名函数每触发一次,就会带来一个定时器,还是会各自计时,没有实现防抖。点第二次要把前一次的定时器销毁,怎么销毁定时器呢?
有没有办法,可以实现点第二次的时候可以拿到上一次的定时器,然后把上一次定时器销毁?
我们在debounce
函数里面定义一个let timer = null
,并在匿名函数里面将setTimeout()
的值赋给timer
。(debounce
执行完后,let timer
不会销毁,而是留在闭包里 ) 从始至终debounce
只执行了一次,随着用户多次点击而执行的是debounce
返回的函数体(那个匿名函数),第二次执行匿名函数时,那个timer
其实就是上一次的定时器。如果time
有值就用clearTimeout(timer)
把它清除掉,也就实现了销毁上一次定时器。
xml
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式
function handle (){
console.log('向后端发送请求');
}
function debounce(fn,wait){ //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
let timer = null
return function(){
if(timer) clearTimeout(timer) //清除上一次的定时器
timer = setTimeout(()=>{
fn()
},wait);
}
}
</script>
</body>
这样一来,就成功实现了防抖。但写成这样还是不行,还有很多东西没有考虑到。
2.修改原函数的this指向
先问一个问题,以下代码hadle
里面的this
指向谁?
xml
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", handle);
function handle (){
console.log('向后端发送请求');
console.log(this);
}
</script>
handle
不是独立调用的,是btn.addEventListener
帮它调用的,所以this
不可能指向window
。
看一下浏览器的打印就明白了,handle
是被btn
调用的,this
指向btn
(btn是dom对象)(隐式绑定)
而代码如果写成以下这样,handle
就是独立调用,handle
里面的this
就指向了window
了,显然不合理,写防抖不能修改原函数的this
指向。
xml
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 想到子父组件传值
function handle (){
console.log('向后端发送请求');
console.log(this);
}
function debounce(fn,wait){ //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
let timer = null
return function(){
if(timer) clearTimeout(timer) //清除上一次的定时器
timer = setTimeout(()=>{
fn()
},wait);
}
}
</script>
现在我们要来解决这个问题,利用call
把hande
里面的this
重新指向btn
。加上fn.call(this)
这行代码就可以了。
setTimeout
里面的this
其实就指向btn
。因为箭头函数没有this
,this
指向匿名函数,而匿名函数又相当于写在addEventListener
里面,,所以匿名函数的this
就是指向btn
。
scss
function debounce(fn,wait){
let timer = null
return function(){
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn.call(this) //把handle里面的this重新指向btn
},wait);
}
}
但是写成这样还不够,还没有考虑到handle
事件的参数。
3.不能影响原函数的参数
什么是事件参数?
当函数被绑定在一个事件上执行时,就一定会具有一个形参,用来描述当前的事件详情。
xml
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", handle);
function handle (e){
console.log('向后端发送请求');
console.log(e);
}
</script>
以下是我们之前写的防抖代码,并没有考虑到handle
的参数问题
xml
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件 发布订阅模式 想到子父组件传值
function handle (e){
console.log('向后端发送请求');
console.log(e);
}
function debounce(fn,wait){ //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
let timer = null
return function(){
if(timer) clearTimeout(timer) //清除上一次的定时器
timer = setTimeout(()=>{
fn.call(this)
},wait);
}
}
</script>
这样
handle
打印出来的事件参数e
是undefined
,显然不合理,现在来解决这个问题。
debounce
函数return出来的那个匿名函数是可以接受到事件参数e
的,再通过fn.call(this,e)
把e
传给fn
就可以了。
scss
function debounce(fn,wait){
let timer = null
return function(e){
if(timer) clearTimeout(timer)
timer = setTimeout(()=>{
fn.call(this,e)
},wait);
}
}
这样handle
就接受到了事件参数了。
但还是要再优化一下,万一handle
还能接受很多参数呢
scss
function debounce(fn,wait){ //防抖工具函数 为了符合 addEventListener语法,要返回一个函数体
let timer = null
return function(...args){
if(timer) clearTimeout(timer) //清除上一次的定时器
timer = setTimeout(()=>{
fn.call(this,...args)
},wait);
}
}
至此,防抖大功告成!以下是完整的手写防抖代码:
xml
<button id="btn">提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click", debounce(handle,1000));// 订阅一个click事件
function handle (e){
console.log('向后端发送请求');
console.log(e);
console.log(this);
}
function debounce(fn,wait){ //防抖工具函数
let timer = null
return function(...args){
if(timer) clearTimeout(timer) //清除上一次的定时器
timer = setTimeout(()=>{
fn.call(this,...args) //把handle里面的this重新指向btn,并且接受参数
},wait);
}
}
</script>
二、节流
在规定时间内只执行一次。
节流和防抖很相似,我们定义一个throttle
函数。函数内部定义一个let preTime = null
记录上次点击的时间戳(throttle
执行完后,let timer
不会销毁,而是留在闭包里)
每次点击用Date.now()
拿到当前时间戳,用现在时间戳nowTime
减去上一次点击的时间戳preTime
得到时间间隔,如果时间间隔大于规定时间就执行,否则就不执行。
以下是手写节流的代码
xml
<button id="btn" >提交</button>
<script>
let btn = document.getElementById("btn");
btn.addEventListener("click",throttle(handle,1000));
function handle (e){
console.log('向后端发送请求');
console.log(this);
}
function throttle(fn,wait) { // 节流函数
let preTime = null;
return function(...args){
nowTime = Date.now()
if(nowTime - preTime > wait){
fn.call(this,...args)
preTime = nowTime
}
}
}
</script>
这样就成功实现了节流:
三、总结
不管是手写防抖还是节流,记住以下3点就可以了。
- 要有防抖(节流)效果
- 不能修改原函数的this指向
- 不能影响原函数的参数