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次。异步代码与异步调度器的所需的封装相同。