BFE.devAI解答 - JavaScript 代码题
7. 手写debounce()并支持leading 和 trailing
解题思路:
- 和debounce类似,我们需要定义一个守卫函数来决定延迟后函数执行的方案。
- 然后需要根据
leading
和trailing
来判断如何处理函数的执行。如果leading
是true
,则在起始时间执行函数,在trailing
是true
的情况下,函数延迟执行。 - 判断这些条件并实现它们,我们需要使用条件和三元操作符多次运算。一些标准的算术操作在其中也很重要。
初学者实现代码如下(打开注释):
function debounce(callback, time, options) {
let timeoutId;
return function(...args) {
const leadingResult =
options?.leading && !timeoutId
? callback.apply(this, args)
: undefined;
clearTimeout(timeoutId);
timeoutId = setTimeout(() => {
const trailingResult =
options?.trailing && !options?.leading
? callback.apply(this, args)
: undefined;
timeoutId = undefined;
console.log({ leadingResult, trailingResult }); // log result
}, time);
console.log({ leadingResult }); // log result
return leadingResult; // return result
};
}
完整代码:
function throttle(callback, time, options) {
let shouldWait = false;
let timerId = undefined;
return function(...args) {
const shouldExecuteLeading = options?.leading && timerId === undefined;
const shouldExecuteTrailing =
!shouldWait && options?.trailing && timerId !== undefined;
const shouldExecute =
shouldExecuteLeading || shouldExecuteTrailing;
if (shouldExecute) {
callback.apply(this, args);
timerId = setTimeout(() => {
timerId = undefined;
}, time);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
callback.apply(this, args);
timerId = undefined;
}, time);
}
shouldWait = true;
setTimeout(() => {
shouldWait = false;
}, time);
};
}
可以看到我们添加了两个参数:
function throttle(callback, time, options) {
...
}
我们用一个布尔值来确认函数是否可以执行:
let shouldWait = false;
当然这个值最开始是false
的。这个值不能只在定时器里使用,而是在下一次瞬间也需重置。
执行函数的前提是满足leading
或trailing
两个条件之一。我们需要检查 leading
的情况并判断定时器是否存在.
const shouldExecuteLeading = options?.leading && timerId === undefined;
这里它确实需要是等于undefined
还是null
.
如果它们不同到底表示什么并且为何?如果经过允许它等于null
的测试,那么null
会被错误地解释为函数未缓存,出现了一个额外的错误。由此得出结论:undefined
是唯一的正确选项.
然后,我们应该在没有延迟的情况下允许trailing处理。如果函数已经被delay,就不需要再激活它了。
const shouldExecuteTrailing =
!shouldWait && options?.trailing && timerId !== undefined;
我们现在可以用它定义判断逻辑:
const shouldExecute =
shouldExecuteLeading || shouldExecuteTrailing;
if (shouldExecute) {
// execute immediately
} else {
// delay
}
现在让我们来看看到底是哪里执行的。
Leading activation
leading回调函数的调用:
if (shouldExecuteLeading) {
callback.apply(this, args);
timerId = setTimeout(() => {
timerId = undefined;
}, time);
}
这里真的很简单:执行函数,并将计时器建立绑定的函数进行更新。
Trailing activation
callback函数移动到轮廓线下的时间取决于你地等待时间。
if (!shouldWait) {
if (shouldExecuteTrailing) {
clearTimeout(timerId);
callback.apply(this, args);
timerId = setTimeout(() => {
timerId = undefined;
}, time);
} else {
clearTimeout(timerId);
timerId = setTimeout(() => {
callback.apply(this, args);
timerId = undefined;
}, time);
}
}
我们从条件中剥离了其他变量,这使得它更具意义。如果我们需要激活trailing状态的回调函数,我们必须从调度器中拿出定时器,触发函数调用,以便后续函数不会再执行。
对于拖尾函数,我们现在有两种方式定义:
- 如果其满足
shouldExecuteTrailing
条件,就直接立即激活。 - 如果这不是
leading callback
,等待后执行。
无论我们以哪种方式执行函数,最后一步都是将计时器重置为undefined:
timerId = undefined;
请注意,无论leading状态如何,每个计时器都需要执行delay,因为正如我之前所说的,前一个可能被完全延迟,进而时间增加。
如果要重置状态,则需要将一个should等待的布尔值延迟到下一个时间周期:
shouldWait = true;
setTimeout(() => {
shouldWait = false;
}, time);
这样做让我们清楚什么时候再次开始。可能最好不要延迟,因为问题只会变得更多。但是,如果在下一次的“形状线下方”例如需要10毫秒,我们在一次测试中看到只有13个重复。我们从来没有达到过100次。异步代码与异步调度器的所需的封装相同。