防抖

你是否在日常开发中遇到一个问题,在滚动事件中需要做个复杂计算或者实现一个按钮的防二次点击操作。
这些需求都可以通过函数防抖动来实现。尤其是第一个需求,如果在频繁的事件回调中做复杂计算,很有可能导致页面卡顿,不如将多次计算合并为一次计算,只在一个精确点做操作。
PS:防抖和节流的作用都是防止函数多次调用。区别在于,假设一个用户一直触发这个函数,且每次触发函数的时间小于设定的间隔时间,防抖的情况下只会调用一次,而节流的情况会每隔一定时间(设定的延迟时间)调用函数。

我们先来看一个袖珍版的防抖理解一下防抖的实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @param {function} func 回调函数
* @param {number} delay 函数调用的间隔
* @param {} arg 传入回调函数的参数
* @return {function} 返回客户调用函数
*/
const debonce = (func, delay = 100, ...args) => {
let timer = null
// 这里返回的函数是每次用户实际调用的防抖函数
return function () {
// 如果已经设定过定时器了就清空上一次的定时器,重新设置新的定时器,并不会执行之前定时器里的函数了
if (timer) clearTimeout(timer)
timer = setTimeout(() => {
func.apply(this, args)
}, delay)
}
}

这个简单版的防抖有个缺陷,它只能在延迟过后调用。我们有时候需要立即调用回调函数,比如点击获取验证码就可以看做是延迟时间比较久的立即执行的防抖函数,现在我们试着做个立即执行版。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @param {function} func 回调函数
* @param {number} delay 函数调用的间隔
* @param {} args 传入回调函数的参数
* @return {function} 返回客户调用函数
*/
const debonce = (func, delay = 100, ...args) => {
let timer = null
return function () {
if (timer) {
// 如果此时已经有定时器,清除它
clearTimeout(timer)
} else {
// 如果没有,马上执行回调函数
func.apply(this, args)
}
// 不管此时有没有定时器,都要重设一遍
timer = setTimeout(() => {
timer = null
}, delay)
}
}

我们也可以将两个版本合并起来,多传入一个参数来判断其是哪一种防抖函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
/**
* 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 delay,func 才会执行
*
* @param {function} func 回调函数
* @param {number} delay 表示时间窗口的间隔
* @param {boolean} immediate 设置为ture时,是否立即调用函数
* @return {function} 返回客户调用函数
*/
const debonce = (func, delay = 100, immediate = true, ...args) => {
// 声明后面会用到的定时器和执行上下文
let timer, context
// 延迟方法
later = () => setTimeout(() => {
// 重置定时器
clearTimeout(timer)
timer = null
// 如果不是立即执行函数,延时后执行回调
if (!immediate) {
func.apply(context, args)
}
}, delay)
return function () {
if (!timer) {
if (immediate) {
// 没有定时器的立即执行函数会立即执行回调
func.apply(this, args)
} else {
// 没有定时器的非立即执行函数存入当前的执行上下文
context = this
}
// 不管是那种防抖函数,都要执行延时函数,并存入定时器
timer = later()
} else {
// 如果还有定时器,重置定时器
clearTimeout(timer)
timer = later()
}
}
}

整体函数实现的不难,总结一下。

  • 对于按钮防点击来说的实现:如果函数是立即执行的,就立即调用,如果函数是延迟执行的,就缓存上下文和参数,放到延迟函数中去执行。一旦我开始一个定时器,只要我定时器还在,你每次点击我都重新计时。一旦你点累了,定时器时间到,定时器重置为 null,就可以再次点击了。
  • 对于延时执行函数来说的实现:清除定时器 ID,如果是延迟调用就调用函数

节流

防抖动和节流本质是不一样的。防抖动是将多次执行变为最后一次执行,节流是将多次执行变成每隔一段时间执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* @param {function} func 回调函数
* @param {number} duration 回调函数调用的间隔
* @param {} arg 传入回调函数的参数
* @return {function} 返回客户调用函数
*/
const throttle = (func, duration, ...args) => {
// previous 表示上次调用回调函数的时间
let previous = 0
return function () {
// current 表示这次触发节流函数的时间
let current = new Date()
// 当这次触发节流的时间与上次调用回调函数的时间相差大于等于设定的时间间隔时,触发回调函数,并设置新的调用回调函数的时间
if (current - previous >= duration) {
func.apply(this, args)
previous = current
}
}
}