[toc]
手写防抖和节流
摘要: 防抖和节流是前端性能优化上非常重要的技术,如何实现一个防抖和节流,不仅能轻松拿捏面试官,更能在开发中提高应用性能
1. 前言
防抖和节流是前端性能优化上一个非常重要的技术,用得恰当可以很好的提高前端应用的性能。而这两个技术除了经常会在面试当中被问到其中原理以及让面试者手动的实现外,更多的是在实际的开发中它的确相当常用。
首先,需要明白防抖和节流的目的:在高频率触发事件下,为了导致大量计算或异步请求,从而造成的性能或者卡顿问题。。
举一个实际的例子:当页面很长时,一般会实现一个回到顶部的按钮,按钮的实现通常采用监听滚动条所在的位置,当大于某个值(如 1000)时就出现。这里就以获取滚动条位置为例说明,一般会如下实现:
html
<body>
<script>
function getScrollPosition() {
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
console.log('滚动条位置:' + scrollTop);
}
window.onscroll = getScrollPosition;
</script>
</body>
当滑动滚动条时,getScrollPosition
方法会,如下:
这里只是一个简单的控制台打印,试想一下,**如果这里是一个复杂的逻辑计算呢?又或者说这里是向服务器发起 ajax 请求呢?**如果不做处理的话,后果将不堪设想。这时候防抖和节流技术就可以发挥其真正的实力了,下面就来详细的讲解如何自己手动实现一个防抖和节流。
2. 防抖(debounce)
基于以上场景,滚动条一旦滚动就会立即触发对应的回调函数,那是否可以有一个方法能够实现当一直处于滑动状态时便不去执行回调函数,而是等到不滑动了再去执行,这样便不会执行多次了。比如在停下来 200ms 或者自定义一个时间后才去执行一次,这样就不会触发很多次了。这个想法就是常说的 防抖。
一直处于滑动状态:有时候可能会稍微的停顿,但如果停顿的时间很短,短于设置的 “停下来后执行回调函数的时间(200ms 或者自定义一个时间)”,也认为是一直处于滑动状态的。
按照这个思路,整体原理和实现也是呼之欲出了:
- 一旦停止触发了(在滑动滚动条的场景就是一旦停止滑动了),立即设置一个定时器用于触发真正的回调函数并保存下来,定时器的时间为期望停止触发事件后间隔多久真正执行回调函数的时间(例如 200ms 或者任意时间,建议不可太短也不可太长,太短防抖意义不大,太长体验不佳)
- 当在间隔时间以内再次触发事件,那么便清除前一个定时器并设置一个新的定时器
问题来了:每一次都在调用防抖函数,那么如何记录前一个定时器呢?,这也是面试官考察实现防抖节流的一个重要原因。利用 js 闭包的机制可以维护一个私有变量,这个私有变量便可以用来保存定时器。
因此,利用 setTimeout
来计时和执行回调函数以及 clearTimeOut
来清除定时器,再加上闭包的机制记录定时器便可轻松实现,话不多说,上代码。
js
/**
* 防抖函数
* @param {*} fn 需要执行的事件(操作)
* @param {*} delay 停止触发事件多久后真正执行回调
*/
function debounce(fn, delay) {
let timer = null; // 闭包私有变量记录定时器
return function (...args) {
// 回调函数的参数接收
clearTimeout(timer); // 清除定时器,不论是否存在上一个定时器,直接清除
timer = setTimeout(() => fn(...args), delay); // 创建新的定时器
};
}
代码相当的简洁,仅仅只有 4 行,值得细细品味,不过代码初始时并非 4 行,可 点击查看 实现过程代码。简单测试如下:
html
<script>
function getScrollPosition(event) {
const scrollTop =
document.documentElement.scrollTop || document.body.scrollTop;
console.log('滚动条位置:', scrollTop, '滚动事件:', event);
}
window.onscroll = debounce(getScrollPosition, 1000);
</script>
运行代码你会发现,这时候如果持续滚动滚动条,获取滚动条位置的事件是不会触发的,而是会等到停止触发 1000ms 以后才会执行,代码中注释已经做了详细的解释了。
3. 节流(throttle)
防抖的确能够避免频繁触发回调,但是如果我们希望在滑动的过程中,每隔一段时间至少执行一次呢?又当如何呢?
举个栗子,长页面中通过 ajax 回调去获取图片,如果用户一直滑动不停,采用防抖的话,页面会一直是空白的,而实际上希望的是,虽然是在一直滑动不停,但是每隔一段时间还是发起一次 ajax 请求去获取图片,这样不会让页面一直是空白状态。另一个栗子,用户在输入框中不断输入,在键盘按下时发起 ajax 请求数据,如果不做处理,用户输入过快将发起大量的 ajax,此时如果采用防抖的方式达不到预期的用户一边输入一边请求数据的目的,此时呢,节流便出场了。
按照每隔一段时间执行一次回调函数的思路,具体实现步骤如下:
- 记录前一次执行的时间(和防抖中记录前一次定时器的实现机制一致,闭包)
- 触发事件时判断当前时间和前一次执行的时间差是否大于间隔时间;如果大于,说明应该再次调用回调函数执行了,如果小于,说明间隔时间还不够, 直接忽略即可。
js
/**
* 节流函数
* @param {*} fn 需要执行的事件(操作)
* @param {*} interval 每隔 interval 这段时间都需要执行一次
*/
function throttle(fn, interval) {
let record = Date.now(); // 记录前一次调用的时间
return function (...args) {
const now = Date.now();
if (now - record > interval) {
// 当当前时间和上一次调用时间间隔大于间隔时间时,执行回调函数
fn(...args);
record = now; // 调用时重新赋值
}
};
}
上述实现方式整体没有问题,但是存在一个需要关注的点:回调函数。这样可能会导致用户在触发事件时便会立即执行回调函数,因为程序调用节流函数的时间一定是在第一次触发事件的时间之前并且可能相隔很久。
不过这并不是问题,看需求而定:如果需求就是第一次触发事件时就需要执行一次回调函数,那如此便满足;如果需求是需要以用户第一次触发事件为起始时间进行节流,那这就不满足了。
为了能够实现 “回调函数第一次是否触发的判断的起始时间是用户第一次触发事件的时间” 这一目的,此时需要转换一个思路:
- 利用闭包记录在间隔期间是否调用过回调函数
- 未调用过的情况下利用定时器来执行回调函数
实现代码如下:
js
function throttle(fn, interval) {
let done = false; // 记录在间隔期间是否调用过回调函数
return function (...args) {
if (!done) {
// 在间隔期间没有调用过回调函数
done = true; // 在上一次定时器未结束前 done 均为 true,即在这个期间已经调用过一次回调函数了
setTimeout(() => {
// 利用定时器来执行回调函数
fn(...args);
done = false; // 开启下一次执行回调函数的阀门
}, interval);
}
};
}
4. 应用场景
- 防抖技术
- 页面
resize
事件,用户在放大或缩小页面时不执行真正的回调函数,等到用户停止缩放时,再去执行 - 页面
scroll
事件,例如回到顶部按钮 - 搜索框
keyup keydown
事件,输入完成后才真正执行回调函数
- 页面
- 节流技术
- 搜索框
keyup keydown
事件,用户边输入就边去发起 ajax 请求数据,采用节流技术,每间隔一段时间就去执行一次 - 页面
scroll
事件,长页面展示图片时,向下划动发起 ajax 请求图片
- 搜索框
5. 总结
防抖和节流输入前端中非常重要的性能优化技术,理解透彻,相信不论是在面试还是在开发中,都能受益。关于这两个技术,简单的说一下异同点:
相同点:
在高频率触发事件下,为了导致大量计算或异步请求,从而造成的性能或者卡顿问题。
异同点:
- 防抖:当触发事件结束后才真正的执行对应的事件,中间不执行
- 节流:每隔一段时间都去执行一次事件(当然是在触发的过程中)