lodash之debounce实现

防抖,节流 这都是一些老生常谈的问题,作为一个前端也算是必备的知识了.

通常工具类的函数,我们一般都是直接使用一个库 lodash. 当然了,自己写一个简易版本的也不是不能,不过参考 lodash 源码之后,就发现人家写的考虑的相当周全.

话不多说,最简易版本走一个

1
2
3
4
5
6
7
8
9
10
11
12
13
// v1.0
function debounce(func, wait) {
let timer = undefined;
return function () {
if (timer) {
clearTimeout(timer);
}
timer = setTimeout(func, wait);
};
}
const fn = debounce(() => {
console.log("1s 后执行");
}, 1000);

如你的要求没那么多,这个就已经能满足你的需求了, 但如果你在使用中发现 this 怎么都指向 window or globalThis, 这个时候,就需要稍微升级一下了.

1
2
3
4
5
6
7
8
9
10
11
12
// v2.0
function debounce(func, wait) {
let timer = undefined;
return function () {
const that = this,
args = arguments;
timer && clearTimeout(timer);
timer = setTimeout(() => {
func.apply(that, args);
}, wait);
};
}

这个时候,就能保持 this 不变了.

有时候,我们需要至少执行一次,之后再进行防抖. 那么就需要小小的升级一下. 加上一个参数 配置项(opt.immediately),来判断是否需要立即执行一次

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//v2.1 改进一下, 增加立即执行判断
function debounce(func, wait, opt = { immediately: false }) {
let timer = undefined;
return function () {
let that = this,
args = arguments;
if (timer === undefined && opt.immediately) {
func.apply(that, args);
}
timer && clearTimeout(timer);
timer = setTimeout(() => {
func.apply(that, args);
}, wait);
};
}

const run = debounce(
() => {
console.log("OK");
},
1000,
{ immediately: true }
);

lodash 相比,这个防抖函数,还有好多功能未实现, 我把源码整理了一下, 把debounce 以及依赖的函数整理了一下,贴了出来. 如果算上依赖的函数和属性等,将近有 250 行代码了. 核心函数 debounce 差不都 100 多行, 占一半左右吧, 最后还有一个 节流函数, 其实就是调用的 debounce, 改了下参数.

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
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
// debounce配置项如下
// const options = {
// leading: boolean, leading表示在wait时间开始时 立即执行回调
// maxWait: number, 表示func可以延迟执行的最大时间,若超过了maxWait,则必须要执行回调函数func。
// trailing: boolean, trailing表示在wait时间结束后 立即执行回调
// };

var nullTag = "[object Null]",
symbolTag = "[object Symbol]",
undefinedTag = "[object Undefined]";

var reIsBinary = /^0b[01]+$/i;
var reIsOctal = /^0o[0-7]+$/i;
var reIsBadHex = /^[-+]0x[0-9a-f]+$/i;
var nativeMax = Math.max;

var freeParseInt = parseInt;

var freeGlobal =
typeof global == "object" && global && global.Object === Object && global;

var freeSelf =
typeof self == "object" && self && self.Object === Object && self;

var objectProto = Object.prototype;

var root = freeGlobal || freeSelf || Function("return this")();

var FUNC_ERROR_TEXT = "Expected a function";
var ctxNow = Date && Date.now !== root.Date.now && Date.now;
var nativeObjectToString = objectProto.toString;

var symToStringTag = Symbol ? Symbol.toStringTag : undefined;
var reTrim = /^\s+|\s+$/g;

var now =
ctxNow ||
function () {
return root.Date.now();
};

function objectToString(value) {
return nativeObjectToString.call(value);
}

function getRawTag(value) {
var isOwn = hasOwnProperty.call(value, symToStringTag),
tag = value[symToStringTag];

try {
value[symToStringTag] = undefined;
var unmasked = true;
} catch (e) {}

var result = nativeObjectToString.call(value);
if (unmasked) {
if (isOwn) {
value[symToStringTag] = tag;
} else {
delete value[symToStringTag];
}
}
return result;
}

function baseGetTag(value) {
if (value == null) {
return value === undefined ? undefinedTag : nullTag;
}
return symToStringTag && symToStringTag in Object(value)
? getRawTag(value)
: objectToString(value);
}

function isSymbol(value) {
return (
typeof value == "symbol" ||
(isObjectLike(value) && baseGetTag(value) == symbolTag)
);
}

function toNumber(value) {
if (typeof value == "number") {
return value;
}
if (isSymbol(value)) {
return NAN;
}
if (isObject(value)) {
var other = typeof value.valueOf == "function" ? value.valueOf() : value;
value = isObject(other) ? other + "" : other;
}
if (typeof value != "string") {
return value === 0 ? value : +value;
}
value = value.replace(reTrim, "");
var isBinary = reIsBinary.test(value);
return isBinary || reIsOctal.test(value)
? freeParseInt(value.slice(2), isBinary ? 2 : 8)
: reIsBadHex.test(value)
? NAN
: +value;
}

function isObjectLike(value) {
return value != null && typeof value == "object";
}

function isObject(value) {
var type = typeof value;
return value != null && (type == "object" || type == "function");
}

function debounce(func, wait, options) {
var lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime,
lastInvokeTime = 0,
leading = false,
maxing = false,
trailing = true;

if (typeof func != "function") {
throw new TypeError(FUNC_ERROR_TEXT);
}

wait = toNumber(wait) || 0;

if (isObject(options)) {
leading = !!options.leading;
maxing = "maxWait" in options;
maxWait = maxing
? nativeMax(toNumber(options.maxWait) || 0, wait)
: maxWait;
trailing = "trailing" in options ? !!options.trailing : trailing;
}

// 执行回调函数, 清理上次的 this 和 参数
function invokeFunc(time) {
var args = lastArgs,
thisArg = lastThis;

lastArgs = lastThis = undefined;
lastInvokeTime = time;
result = func.apply(thisArg, args);
return result;
}

// leading 检查判断 同时开启定时器 ,记录上次执行时间 lastInvokeTime
// 如果 leading = true, 立即执行, 否则 返回结果,这个时候 result 可能还是 undefined.

function leadingEdge(time) {
// Reset any `maxWait` timer.
// 这里并 不一定会执行 回调函数(func), 但是也设置了一个值,
// 就是为了判断 最大间隔 执行, lastInvokeTime 只会再真正调用 回调函数的时候设置
// 但是 如果 防抖函数 一直触发的情况, 实际回调函数(func) 并不会执行, 所以 lastInvokeTime 一定是空的.
// 如果设置maxWait, 没有上次执行时间, 就无法判断回调函数执行间隔时间,
// 其实这里 就是针对首次的情况, 初始化第一次时间未当前时间

// 如果 不设置 maxWait ,其实没有必要添加,

lastInvokeTime = time;

// Start the timer for the trailing edge.
timerId = setTimeout(timerExpired, wait);
// Invoke the leading edge.
return leading ? invokeFunc(time) : result;
}

// 计算下一次执行的间隔时间(剩余时间)
// 这里就是重启定时器时候,需要重新计算一下,
// 针对与 wait maxWait 不同场景,执行下一次定时器需要的时间

// wait - (当前时间 - debounced 上一次执行时间)
// maxWait - (当前时间 - func 上一次执行时间)

// 然后取最小

function remainingWait(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime,
timeWaiting = wait - timeSinceLastCall;

return maxing
? nativeMin(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting;
}
/**
* 这里对与时间的判断 是否应该调用 回调函数
* 这里的判断很巧妙
*/
function shouldInvoke(time) {
var timeSinceLastCall = time - lastCallTime,
timeSinceLastInvoke = time - lastInvokeTime;

// Either this is the first call, activity has stopped and we're at the
// trailing edge, the system time has gone backwards and we're treating
// it as the trailing edge, or we've hit the `maxWait` limit.

// 1. 函数还未被调用过
// 2. 函数(debounce)距离上次 调用 间隔时间 >= wait 间隔时间
// 3. 函数(debounce)距离上次 调用 间隔时间 < 0 ??这个不知道为啥要这样判断, 按理来说, 当前时间 time 一定会大于上次执行时间
// 4. 设置了最大间隔时间, 而且 回调函数间隔执行时间 大于 最大间隔时间
return (
lastCallTime === undefined ||
timeSinceLastCall >= wait ||
timeSinceLastCall < 0 ||
(maxing && timeSinceLastInvoke >= maxWait)
);
}

// 进入到这个函数(定时器的回调函数)只有两个场景
// 1. 没有定时器
// 2. 定时器被清了,那不是相当于还是没有定时器
function timerExpired() {
var time = now();
// 再根据时间判断一下,能不能执行,
if (shouldInvoke(time)) {
return trailingEdge(time);
}

// 不能执行,重启定时器,不过时间需要重新计算一下
// Restart the timer.
timerId = setTimeout(timerExpired, remainingWait(time));
}

// trailing 的逻辑处理
// 其实这里的英文注释系的很明白了, func 至少被节流过一次,才会触发, 也就是 wait之后,触发一次
// 节流过的话, 会把 参数保存起来 lastArgs

function trailingEdge(time) {
timerId = undefined;

// Only invoke if we have `lastArgs` which means `func` has been
// debounced at least once.
if (trailing && lastArgs) {
return invokeFunc(time);
}
lastArgs = lastThis = undefined;
return result;
}

function cancel() {
if (timerId !== undefined) {
clearTimeout(timerId);
}
lastInvokeTime = 0;
lastArgs = lastCallTime = lastThis = timerId = undefined;
}

function flush() {
return timerId === undefined ? result : trailingEdge(now());
}
/**
* 最终返回的经过防抖动处理的函数
*
* debounced 函数的执行,无非是为了判断 func 是否能执行,因此按照这个分类,可能存在的场景:
*
* - func 可以执行
* 1. 第一次执行 (leading = true)
* 2. maxWait 存在 && 超过了 maxWait 时间
* 3. 定时器到达了wait, debounced 并未触发
*
* - func 不可以执行
* 1. 时间 wait 不满足
* 1.1. 定时器不存在, 初始化一个即可
* 1.2. 定时器已经存在 ,重置定时器, 延迟wait执行.
*
* 2. 不存在 wait 满足这种场景, 如果 wait 满足, func 必定执行
*/

/**
* 如果按照 debounced 的逻辑来判断
*
* - 满足 >wait || >maxWait || 第一次. 前两种立即执行, 第三种可能会立即执行,可能延迟执行,
*
* 1. 没有定时器
* 无非两种情况 立即执行, 定时延期执行
*
* 2. 有定时器, (说明func 已经被加入到执行的 "队列里面", 并非第一次,)
*
* 2.1 如果设置了最大间隔 maxWait,这里结合上一层的条件(> wait , > maxWait). 立即执行,并添加一个定时器,
*
* 2.2 如果没有,无需处理,等定时器执行即可
*
* - 不满足, func 一定不会立即执行
* 1. 有定时器, 已经触发了防抖, 无需处理,到了时间肯定会执行
* 2. 没有定时器, 创建即可
*
*
*/
function debounced() {
var time = now(),
isInvoking = shouldInvoke(time);

lastArgs = arguments;
lastThis = this;
lastCallTime = time;

if (isInvoking) {
// 如果可执行,( 通过时间判断, 满足执行条件)

// 如果没有 定时器, 则检查 leading,
// 可能是第一次 (或者已经执行过 func, timerId 被 清理了 , 也可以认为是新的开始,第一次)
if (timerId === undefined) {
return leadingEdge(lastCallTime);
}

// 如果 有定时器 ( 说明已经触发过了 debounced )

// 如果 设置了最大 间隔时间,
if (maxing) {
// Handle invocations in a tight loop.
// 先把定时器 clear 掉, 再添加一个, 并立即执行 func
clearTimeout(timerId);
timerId = setTimeout(timerExpired, wait);
return invokeFunc(lastCallTime);
}
}
if (timerId === undefined) {
timerId = setTimeout(timerExpired, wait);
}
return result;
}
debounced.cancel = cancel;
debounced.flush = flush;
return debounced;
}

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,
});
}

这其中有一半 依赖的代码,是考虑并处理了 N 多的场景,用来提高代码健壮性.这就是一个通用库的开发与业务开发的区别之一吧。

例如: 对 wait 的类型判断和转换, 获取当前时间的 now 方法(Date 的 now 是可修改的,也就是说,如果有人修改了 now,如果你直接使用 Date.now() 就无法返回正确的时间). 全局对象的获取 , 类型判断, 转换等等.

我研究了一下,感觉他写的不错的地方就是针对于时间的判断与控制,

  1. 通过间隔时间判断, 避免频繁调用和清理定时器,而是放在了 每个定时器执行的时候,再去判断,这个大大的减少了 clearTimeout 的执行.

  2. 增加了函数结果存储, 如果未达到执行时间,可以返回上次的执行结果,

  3. 最大间隔时间选项, 这不就是把 防抖 转成 节流了么???

  4. leading trailing 配置项,可以灵活设置执行回调的时机

看完大佬写的,再看看自己实现的简直天差地别。

后话:虽然大佬写的确实牛,但是在自己项目中,可能还需要实际考量一下,例如:在自己的项目,对于参数类型是可以控制把握的 或者性能要求没有那么高,如果只是为了 使用其中 1,2 个函数,引用一个 lodash,着实没有太大必要, 当然现在有 lodasg-es 版本,能够 treeShaking 按需引用. 即使 treeShaking 之后(我贴出来的代码是用 rollup treeShaking 之后的),依然有 250 多行。比起 10 多行 简易版本仍然大了不少。

当然有轮子的情况下,为啥不用轮子呢?毕竟人家写的是经过时间考验的

努力学习,共勉!!!

作者

Fat Dong

发布于

2023-12-28

更新于

2023-12-28

许可协议