防抖和节流,在日常的开发过程中会经常用到,而在面试中,也是经常会被问的一道手写题。下面就一起复习一下这两个函数的知识点和实现吧。

知识点

在工作中常见的一些场景,比如input搜索框、监听窗口的resize,元素的拖拽,以及滚动条的监听。这些场景的共同点就是事件会被频繁的触发,而频繁的触发会导致资源和性能的大量消耗。通常我们会使用防抖和节流函数来进行优化,那么什么场景该使用防抖,什么时候该使用节流呢?

共同点 区别 应用场景
防抖debounce 在事件频繁被触发时, 只执行最后一次 input输入框、窗口resize、button点击
节流throttle 减少事件执行的次数 有规律的执行 拖拽、scroll

防抖debounce实现

通过知识点我们可以知道,防抖函数的作用是在事件被频繁触发时,只执行最后一次。本质上是延迟执行事件的时机,那么我们可以利用定时器来实现它。

1
2
3
4
5
6
7
8
9
10
// 拆解需求:
// 1. n秒内只能执行一次,所以需要一个setTimeout来控制fn的执行时机。
// 2. 如果触发事件后在n秒内又触发了事件,则重新计算函数延迟执行的时机。
function debounce(fn, delay) {
var timer = null
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
}, delay)
}

上面的代码看似没啥毛病,但实际每次执行的时候,timer都会被重新创建,然后执行完debounce后,里面的变量就会被销毁。这样就会创建出很多个延迟没执行的函数,一到时间就执行了,并不会重新计算时间。

那我们改造一下,把timer提到全局作用域内:

1
2
3
4
5
6
7
var timer = null
function debounce(fn, delay) {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn()
}, delay)
}

通过改造函数虽然也能实现防抖的功能,但是如果有多个防抖的事件,就需要定义多个全局变量,写多个函数,显然这并不是一个靠谱的实现。这时我们需要一个私有变量,并且能在外部访问,那闭包就是一个选择,我们接着改造:

1
2
3
4
5
6
7
8
9
10
function debounce(fn, delay) {
var timer = null
return function() {
if(timer) clearTimeout(timer)
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
}, delay)
}
}

调用debounce函数时,返回了一个匿名函数,由于该函数中还用到了debounce函数中的timer变量,会导致timer无法释放。这样就创建了一个私有变量并可以在外部进行访问。需要注意的是:在合适的时机对闭包进行清除,否则会一直占用内存。

节流throttle实现

同样的原理,只需稍作修改就可以实现节流throttle函数:

1
2
3
4
5
6
7
8
9
10
function throttle(fn, delay) {
var timer = null
return function() {
if(timer) return
timer = setTimeout(() => {
fn.apply(this, arguments)
timer = null
},delay)
}
}

与防抖函数不同的是,节流函数是按一定频率有规律的执行,那么除了定时器以外,是不是还可以用时间差的方式来实现呢?

1
2
3
4
5
6
7
8
9
10
function throttle(fn, delay) {
var last = 0
return function() {
var curr = Date.now()
if(curr - last >= delay) {
fn.apply(this, arguments)
last = curr
}
}
}

小结

防抖和节流函数,我们只需要掌握它的异同点、实现原理、应用场景即可,工作中通常不需要我们去重复的造轮子。有诸如Loadsh这种现成的开源工具,可以很方便的调用。