重要概念-防抖和节流

防抖和节流本质上是优化高频率执行代码的手段。

当浏览器resize/scroll/keypress/mousemove等事件触发时,会不断地调用回调函数,极大地浪费资源、降低前端性能。

↑为了优化体验,需要对此类事件调用次数进行合理限制,这时候我们就可以使用防抖、节流的手段减少调用频率。

防抖-debounce

频繁操作 => 最后一次操作 定时器

定义

n秒后再执行该事件,如果在这期间被重复触发,那么重新计时。

栗子

电梯等人

实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
// 简单实现
function debounced(func, wait) {
let timeout; // 声明一个变量用于保存定时器的返回值,初始化为 undefined
// 我还是喜欢初始化成这样→let timeout = null
return function() { // 返回一个新的函数,这个新函数就是实际使用的防抖函数
let context = this; // 在这个新函数中,保存当前 this 指向
let args = arguments; // 保存当前函数调用时传入的参数

clearTimeout(timeout); // 清除之前设置的定时器,避免函数的过度调用
// clearTimeout 函数并不会抛出异常,因为它可以接受 undefined 作为参数
timeout = setTimeout(function(){ // 设置一个新的定时器,延迟 wait 毫秒执行函数
func.apply(context, args) // 在定时器中执行函数,this 指向上下文为之前保存的 this
}, wait);
}
}

// 加标判断实现——防抖需要立即执行
function debounced(func, wait) {
let timeout
let flag

return function() {
let context = this
let args = arguments;

if (flag) {
func.apply(context, args)
flag = false
}

clearTimeout(timeout)
timeout = setTimeout(function() {
flag = true
}, wait)
}
}

// 传入第三参数实现——防抖需要立即执行
// immediate = true => 首次调用需要立即执行
function debounced(func, wait, immediate) {
let timeout

return function() {
let context = this
let args = arguments

if (timeout) clearTimeout(timeout)
if (immediate) {
let callNow = !timeout
timeout = setTimeout(function() {
timeout = null
}, wait)
if (callNow) {
func.apply(context, args)
}
} else {
timeout = setTimeout(function() {
func.apply(context, args)
}, wait)
}
}
}

// 还有无改进空间?
// 肯定是有,举个例子:可以将函数的返回值进行保存并返回,以便调用者可以取消该函数的延迟执行
function debounce(func, wait, immediate) {
let timeout;

function debounced() {
const context = this;
const args = arguments;

if (timeout) clearTimeout(timeout);
if (immediate) {
const callNow = !timeout;
timeout = setTimeout(() => {
timeout = null;
}, wait);
if (callNow) {
return func.apply(context, args);
}
} else {
timeout = setTimeout(() => {
func.apply(context, args);
}, wait);
}
}

debounced.cancel = function() { // ←这里是核心扩展点啦
clearTimeout(timeout);
timeout = null;
};

return debounced;
}

使用

1
2
3
4
5
6
7
8
9
10
function myFunc() {
....我的函数
}
function debounce() {
....防抖函数
}
let debounceMyfunc = debounce(myFunc, 300, ...其他参数)

debounceMyfunc()
debounceMyfunc.cancel()

场景

防抖在连续的事件,只需触发一次回调的场景有:

  • 搜索框搜索输入。只需用户最后一次输入完,再发送请求。
  • 手机号、邮箱验证输入检测。
  • 窗口大小resize。只需窗口调整完成后,计算窗口大小。防止重复渲染。

节流-throttle

频繁操作 => 少量操作 闭包+延迟器

定义

n秒内只运行一次,如果在这期间重复触发,那么只有一次生效。

栗子

电梯送人

实现

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
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 时间戳
function throttled(func, delay) {
let oldTime = Date.now()
return function() {
let context = this // 个人认为此举更直观
let args = arguments // ↑
let newTime = Date.now()

if (newTime - oldTime >= delay) { // 如果当前时间距离上次执行的时间大于或等于延迟时间,执行相应函数
func.apply(context, args)
oldTime = Date.now()
}
}
}

// 定时器
function throttled(func, delay) {
let timer = null
return function() {
let context = this
let args = arguments
if (!timer) {
timer = setTimeout(function() {
func.apply(context, args)
timer = null // 这个操作很巧妙↓
}, delay)
}
}
}
/*
对于防抖函数来说,用clearTimeout和将timeout设为null的效果是一样的。
但是,对于节流函数来说,应该尽量避免开太多的定时器,因此建议在使用setTimeout时,将定时器变量设置为null。
这样,在函数被调用时,首先判断定时器变量是否为null,如果为null,就创建新的定时器,如果不为null,则不创建新的定时器。如果使用clearTimeout,则需要先清除之前的定时器,再创建新的定时器,这样在高频率调用函数时会开启大量的定时器,导致性能下降。

通常来说,防抖函数相对于节流函数更容易出现创建很多定时器的情况,因为防抖函数在每次触发事件后都会重新创建一个定时器,而节流函数只有在上一次执行的定时器结束后才会创建新的定时器。因此,在高频率触发事件的情况下,防抖函数可能会导致创建大量的定时器,从而影响性能。
*/

// 时间戳 + 定时器 —— 实现更加精确的节流
function throttled(func, delay) {
let timer = null
let startTime = Date.now()

return function() {
let curTime = Date.now()
let context = this
let args = arguments
let remaining = delay - (curTime - startTime)

clearTimeout(timer)
if (remaining <= 0) {
func.apply(context, args)
startTime = Date.now()
} else {
timer = setTimeout(func, remaining)
}
}
}

使用

1
2
3
4
5
6
7
8
function handleScroll(event) {
....滚动事件处理代码
}
function throttled(func, delay) {
....节流函数
}

window.addEventListener('scroll', throttled(handleScroll, 500));

场景

节流在间隔一段时间执行一次回调的场景有:

  • 滚动加载,加载更多或滚到底部监听
  • 搜索框,搜索联想功能

两者的异同

同:

  • 都是为了较低回调的执行频率,节省计算资源
  • 都可以使用定时器实现

异:

  • 防抖着重于一定时间连续触发的事件中只执行最后一次;节流着重于一段时间仅执行一次
  • 防抖是在一段连续操作后执行一次; 节流是每一段时间只执行一次