防抖与节流
# 防抖(debounce)与节流(throttle)都是前台优化必会且常用的方法
# 1.摘要
**相同点:**他们都是控制用户输入频率,从而减少页面函数调用次数的优化方法。(代码极其相似,甚至节流可以当作防抖的一种特殊情况,见lodash源码)
不同点:
debounce是当用户持续操作时(短时间内多次点击按钮,操纵滚动条等),仅触发一次函数调用(大多数为开始操作时与操作结束后任选其一),超过设置时间后才能够再次触发函数调用,这种方式适用于不需要密集多次触发的场景(预防用户手欠或帕金森。。),如调解窗口大小,表单的搜索按钮等
throttle则是在持续操作过程中,以设置时间为间隔,可多次触发函数调用的优化方法。这种方式适用于需要连续触发的场景,如输入框的联想输入,分页切页面等等
# 2.实现
- debounce(触发时执行简单版):
function debounce(fun, delay) {
let timer;
return () => {
if (timer) clearTimeout(timer);
if (!timer) {
fun();
}
timer = setTimeout(() => {
timer = null;
}, delay);
};
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
- debounce触发后执行简单版:
function debounce(fun, delay) {
let timer;
return () => {
if (timer) clearTimeout(timer);
timer = setTimeout(() => {
fun();
}, delay);
};
}
1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
对比来看开始触发的相对复杂些,需要一个标识记载是否已触发过
- throttle(定时器版,触发时执行)
function throttle(fun, delay) {
let timer;
return () => {
if (!timer) {
fun();
timer = setTimeout(() => {
timer = null;
}, delay);
}
};
}
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
我们可以看到节流与防抖很相似,不同的是定时器的位置。
ps. 甚至lodash中throttle直接使用debounce实现,以下会做出代码展示与分析
# 3.lodash
function debounce(func, wait, options) {
/**
* maxWait 最长等待执行时间
* lastCallTime 事件上次触发的时间,由于函数防抖,真正的事件处理程序并不一定会执行
*/
let lastArgs, lastThis, maxWait, result, timerId, lastCallTime
let lastInvokeTime = 0 // 上一次函数真正调用的时间戳
let leading = false // 是否在等待时间的起始端触发函数调用
let maxing = false //
let trailing = true // 是否在等待时间的结束端触发函数调用
// 如果没有传入wait参数,检测requestAnimationFrame方法是否可以,以便后面代替setTimeout,默认等待时间约16ms
const useRAF =
!wait && wait !== 0 && typeof root.requestAnimationFrame === "function"
if (typeof func != "function") {
// 必须传入函数
throw new TypeError("Expected a function")
}
wait = +wait || 0 // wait参数转换成数字,或设置默认值0
if (isObject(options)) {
// 规范化参数
leading = !!options.leading
maxing = "maxWait" in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = "trailing" in options ? !!options.trailing : trailing
}
// 调用真正的函数,入参是调用函数时间戳
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
// 开启计时器方法,返回定时器id
function startTimer(pendingFunc, wait) {
if (useRAF) {
// 如果没有传入wait参数,约16ms后执行
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 取消定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
//等待时间起始端调用事件处理程序
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
// 事件上次触发到现在的经历的时间
const timeSinceLastCall = time - lastCallTime
// 事件处理函数上次真正执行到现在经历的时间
const timeSinceLastInvoke = time - lastInvokeTime
// 等待触发的时间
const timeWaiting = wait - timeSinceLastCall
// 如果用户设置了最长等待时间,则需要取最小值
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
// 判断某个时刻是否允许调用真正的事件处理程序
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
return (
lastCallTime === undefined || // 如果是第一次调用,则一定允许
timeSinceLastCall >= wait || // 等待时间超过设置的时间
timeSinceLastCall < 0 || // 当前时刻早于上次事件触发时间,比如说调整了系统时间
(maxing && timeSinceLastInvoke >= maxWait) // 等待时间超过最大等待时间
)
}
// 计时器时间到期执行的回调
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 重新启动计时器
timerId = startTimer(timerExpired, remainingWait(time))
}
function trailingEdge(time) {
timerId = undefined
// 只有当事件至少发生过一次且配置了末端触发才调用真正的事件处理程序,
// 意思是如果程序设置了末端触发,且没有设置最大等待时间,但是事件自始至终只触发了一次,则真正的事件处理程序永远不会执行
if (trailing && lastArgs) {
return invokeFunc(time)
}
lastArgs = lastThis = undefined
return result
}
// 取消执行
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
// 立即触发一次事件处理程序调用
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
// 查询是否处于等待执行中
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
export default debounce
//Lodash中throttle直接使用debounce实现,说明节流可以当作防抖的一种特殊情况。
function throttle(func, wait, options) {
var leading = true,
trailing = true;
if (typeof func != 'function') {
throw new TypeError(FUNC_ERROR_TEXT);
}
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading;
trailing = 'trailing' in options ? !!options.trailing : trailing;
}
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
});
}
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173