前言
防抖和节流是在前端开发中广泛应用的两种性能优化技术,它们的出现旨在解决一些常见的性能和用户体验问题。在面试中,面试官往往会提及这两个概念,以考察面试者对于性能优化的理解和实际应用的能力。
以下是对这两种技术的深入剖析。
应用场景
下面我用按钮事件来模拟一下:
html
<body>
<button id="btn">提交</button>
<script>
let btn = document.getElementById('btn');
btn.addEventListener('click',function(){console.log('提交了')})
</script>
</body>
我们在页面上创建一个了按钮。当用户点击按钮时,会触发一个事件,例如提交表单或执行某个操作,我们先用打印模拟这个操作。在某些情况下,用户可能会快速点击按钮,比如误触,多点了几下,导致事件被触发多次。
这将带来一系列问题:
重复提交: 如果按钮用于提交表单,快速点击按钮可能导致表单被多次提交,这可能不是你所期望的行为。
性能问题: 如果按钮触发的是某个耗时的操作,比如发送网络请求,快速点击按钮可能导致不必要的性能开销。
这时候就要用到防抖或者节流。
防抖
通过设计防抖函数,可以确保在用户点击按钮后,只有在一定的时间间隔内才会触发相应的事件,比如在用户多次连续点击后,事件只触发一次。
代码实现
js
<script>
let btn = document.getElementById('btn');
function send() {
console.log('提交完成');
}
btn.addEventListener('click',debounce(send,1000))
function debounce(fn,delay) {
let timer;
return function(){
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn()
},delay)
}
}
</script>
代码分析
- 通过
document.getElementById
获取了id为'btn'的按钮元素,将其赋值给变量btn
,以便后续的操作。 function send()
:这是一个用于处理点击事件的函数,用打印来替代实际事件。btn.addEventListener('click', debounce(send, 1000))
:这里我们通过addEventListener
方法为按钮添加了一个点击事件监听器。当按钮被点击时,会执行debounce(send, 1000)
,也就是使用了防抖的send
函数。这里传递了两个参数,第一个是要执行的函数send
,第二个是防抖的时间间隔,这里设置为1秒。function debounce(fn, delay)
:这里我们创建防抖函数的函数。它接收两个参数,fn
是需要防抖的函数,delay
是防抖的时间间隔。- 在
debounce
函数内部,定义了一个变量timer
,用于前一次点击时存储的定时器。 return function()
:因为addEventListener
接收的第二个参数是函数体,所以debounce
需要函数返回新的函数,这个新函数就是实际上会被绑定到按钮点击事件上的函数。if (timer) clearTimeout(timer)
:这行代码表示如果已经存在一个定时器,说明上一次点击的事件还没有触发,所以清除掉这个定时器。timer = setTimeout()
:设置一个新的定时器,重新计算时间,如果一秒钟内没有再次点击,则事件才触发;如果一秒钟之内再次点击,则再次执行该函数。
当我们连续点击按钮的时候,控制台并不会打印,但是当我们停止点击,它在最后一次点击后一秒完成了打印。
效果展示
当我们连续点击,会以最后一个为基准,一秒后执行一次打印。
这样就实现了一个山寨版的防抖功能,何为山寨版?我们知道,在实际应用中,点击事件的触发的时候肯定会传递参数,那么怎么实现呢?
- 我们可以直接在返回的函数内接收这个参数,下面用
e
表示参数:
js
<script>
let btn = document.getElementById('btn');
function send(e) {
console.log('提交完成',e);
}
btn.addEventListener('click',debounce(send,1000))
function debounce(fn,delay) {
let timer;
return function(e){
if (timer) clearTimeout(timer);
timer = setTimeout(function () {
fn(e)
},delay)
}
}
</script>
这样我们确实可以实现一个参数的传递,但是如果传递的参数不止一个,那这种方法就无济于事了。那该怎么实现呢?
在实现之前,我们得先认识arguments
这个概念,它是函数内部的一个特殊对象,用于存储函数调用时传递的所有参数,它是一个类数组对象,具有类似数组的结构,但没有数组的一些方法。我们优化一下,可以得到以下代码:
js
<script>
//接收参数
function send(e,a) {
console.log('提交完成',e,a);
}
btn.addEventListener('click',debounce(send,1000))
function debounce(fn,delay) {
let timer;
return function(){
let args = arguments;
if (timer) clearTimeout(timer);
timer = setTimeout(()=> {
fn.call(this,...args);
},delay)
}
}
</script>
效果展示
成功传入参数。
function send(e)
:我们用这个函数接收一个参数e
。let args = arguments;
:在新函数内部,将传递给新函数的所有参数存储在变量args
中。这是因为在防抖的场景下,我们希望能够传递原始的事件对象给实际执行的函数。timer = setTimeout(() => {)
:设置一个新的定时器,用箭头函数使this
指向外层函数函数(即return
出来的函数)。fn.call(this,...args)
:将this
与fn
函数绑定,并将解构出来的参数传递给要执行的函数。
以上,我们分别完成了不带参数与带参数的防抖函数,我们的主要思想就是,用一个定时器记录这一秒钟内,用户如果重复点击,则以最后一次点击为准,只打印一次。
节流
节流会在一定的时间间隔内固定执行函数,无论触发事件有多频繁,都会按照固定的频率执行。即使在这个时间间隔内有多次触发事件,也只会执行一次。
代码实现
html
<script>
//接收参数
function send(e){console.log('提交了',e);}
btn.addEventListener('click',throttle(send, 1000));
function throttle(fn, delay) {
let prevTime = Date.now();
return function(){
if(Date.now() - prevTime > delay){//第二次点击的时间 - 第一次点击的时间 > 1000
fn.apply(this,arguments);
prevTime = Date.now()
}
}
}
</script>
效果展示
当我们不停点击时,打印会按照固定的频率执行,这里为一秒钟执行一次。
代码分析
前面的部分代码与防抖一致,这里就不过多赘述。
let prevTime = Date.now();
:在throttle
函数内部,定义了一个变量prevTime
,用于存储上一次函数执行的时间戳。if(Date.now() - prevTime > delay){
:通过比较当前时间与上一次函数执行时间的差值,判断是否满足节流的条件,即是否超过了设定的时间间隔。这里我的想法是,如果第二次点击与第一次点击间隔大于1秒,则第二次点击允许执行,如果间隔小于1秒则不允许执行。prevTime = Date.now();
:更新prevTime
,记录下当前函数执行的时间戳,以备下一次判断使用。
在这个按钮事件中,我们实现了带参数的节流函数,我们的主要思想是,如果第二次点击与第一次点击间隔大于1秒,则第二次点击允许执行,如果间隔小于1秒则不允许执行。所以如果你不停点击按钮,它会按照固定的频率执行。
最后
综合来看,防抖和节流是前端开发中常用的两种优化手段,它们在处理函数执行频率方面都发挥着重要的作用。在实际项目中,根据具体的需求和交互场景选择合适的优化方式十分关键。所以面试官常常会叫你手写防抖或节流。
希望这篇文章对你有帮助!
我的Gitee: CodeSpace (gitee.com)
技术小白记录学习过程,有错误或不解的地方还请评论区留言,如果这篇文章对你有所帮助请 "点赞 收藏+关注" ,感谢支持!!