在不看 underscore 源码之前,我自己写的一版可能是这样的。
function debounce(func,wait = 1500) {
let timeout;
function later() {
timeout = setTimeout(() => {
func();
clearTimeout(timeout);
timeout = null;
}, wait)
}
return function debounced() {
if (timeout) {
clearTimeout(timeout);
timeout = null;
later();
} else {
later();
}
}
}
可能很多同学和我一样,都使用 clearTimeout
去中途中断定时器,但是 _.debounce
却没有,它是怎么做到的?
分析过源码之后,才知道,它是用当前时间戳和上一次触发 debounced
的时间戳做比较来确定的。
我们把上一次的时间戳记作 previous
,当前的时间戳记作 now
,now - previous
的差值记作 passed
。
每次重新触发 debounced
时,previous
的时间戳都会更新,而此时我们计算 passed
的规则也就变化了,此时仅仅通过比较 wait - passed
就能知道要不要执行了。 从而避免了 clearTimeout
这一步。
如果把这个思路应用到我们写的第一版上去,就会变成:
function now() {
return Date.now();
}
function debounce(func,wait = 1500) {
let timeout, previous;
function later() {
const passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
func();
}
}
return function debounced() {
previous = now()
if (!timeout) {
timeout = setTimeout(later, wait)
}
}
}
理清楚了这个思路后,剩下的代码相对比较简单,只是一些扩展 API 的代码,相信大家也都能自己看明白,只不过有一行代码刚看会比较懵,我给大家解释一下。
这是它的 later
函数,大家请看倒数第三、四行
var later = function() {
var passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
func.apply(context, args);
// This check is needed because `func` can recursively invoke `debounced`.
if (!timeout) args = context = null; // 这一行。
}
};
我们上一步不是让 timeout = null
了吗,怎么下一步还要有 if (!timeout)
这个判断?这个肯定成立呀!
大家觉得这个判断不必要的原因可能是:这一段并不是异步代码,执行了上一语句肯定就直接执行下一条语句了,所以,这个判断没有必要。
我看过其他同学的源码解析,好像都没有比较详细的说明这一个过程,甚至有些同学说这行代码没有必要,其实是很有必要的。
下面我就来带大家分析一下这个过程。
我们只要举一个反例就好了,下面我们来设计一个例子:
我们写一个简单的递归函数:
let i = 0;
const debouncedFn = debounce(() => {
if (i >= 10) {
return;
}
i++;
debouncedFn()
})
同时把 later
打个标记:
function later() {
const passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
func();
if (!timeout) {
console.log('调用这里-1')
} else {
console.log('调用这里-2')
}
}
}
我们惊奇的发现 '调用这里-2' 被调用了 10 次。
这时候我们就发现了这个判断的必要性,因为我们 debounce
的第一个参数 func
,也可能递归的调用 func
的 debounced 版 。如果不加这个判断的话,就会出现无法递归调用的问题。
这一点,如果我不看它的源码,我肯定不会想到还有这种场景。而我不动手写一遍,也肯定理解不了它的真正用意。总的来说,还是很有收获的。
本篇文章就着重分析了 debounce
的两个点:
- 使用时间戳来判断执行点
- 考虑递归函数的场景
希望能帮助到你,谢谢阅读!
_.debounce
的源码如下:
function debounce(func, wait, immediate) {
var timeout, previous, args, result, context;
var later = function() {
var passed = now() - previous;
if (wait > passed) {
timeout = setTimeout(later, wait - passed);
} else {
timeout = null;
if (!immediate) result = func.apply(context, args);
// This check is needed because `func` can recursively invoke `debounced`.
if (!timeout) args = context = null;
}
};
var debounced = restArguments(function(_args) {
context = this;
args = _args;
previous = now();
if (!timeout) {
timeout = setTimeout(later, wait);
if (immediate) result = func.apply(context, args);
}
return result;
});
debounced.cancel = function() {
clearTimeout(timeout);
timeout = args = context = null;
};
return debounced;
}