简介
debounce
和 throttle
,用中文描述的话,就是 去抖
和 节流
。
它们有什么用:
针对一些
执行频率非常高
的交互或事件,做性能优化。
二者的概念,网上的说法很多,这里不描述了。我这里主要分析下他们的相同点和不同点,和在什么时候用它们。
debounce
和 throttle
的相同点:
都是利用函数延迟执行来实现效果,我们暂时可以理解成用
setTimeout
注:lodash 中为了优化性能,在没有传入 wait 参数的情况下,优先使用 requestAnimationFrame,如果浏览器不支持,再降级使用 setTimeout
debounce
和 throttle
的不同点:
debounce
有一个等待时长,如果在这个等待时长内,再次调用了函数,就取消上一个定时器,并新建一个定时器。所以debounce
适用于input
,keyup
,keydown
等事件, 亦或者click
事件需要防止用户在某个时间范围内多次点击的时候,也可以用。注:在lodash
的实现中 中并没有取消新建
定时器的做法,是用时间来判断的。
throttle
也有一个等待时长,每隔一段这个等待时长,函数必须执行一次。如果在这个等待时长内,当前延迟执行没有完成,它会忽略接下来调用该函数的请求,不会去取消上一个定时器。所以throttle
适用于scroll
,mousemove
等事件。在lodash
的实现中,还有一个等待的最大时长,这个我们分析源码时再讨论。
resize
事件,使用debounce
或throttle
都行,看你的需求啦。
举个栗子,使用 lodash
处理 resize
事件时,在 wait
参数不是非常小的情况下:
用debounce
的话,会在用户停止改变
浏览器窗口大小时触发,也就是只是在最后触发一次。
用throttle
的话,会在用户改变浏览器窗口大小的过程中,每隔一段时间触发一次。
其实在 lodash 的实现中: throttle
就是一个定义了最大等待时长的 debounce
。
接下来,我们先自己实现 简易版的 debounce
和 throttle
,然后再分析lodash
源码中的对应方法
debounce
debounce
有个很重要的特性,就是在规定的等待时长内如果再次调用函数,会取消
上一次函数执行。
所以我们这里可以用 clearTimeout
先清除定时器,再重新 setTimeout
建一个新的定时器。
它的原理其实就是在闭包内维护一个 定时器
。
function debounce(fn, wait) {
let callback = fn;
let timerId = null;
function debounced() {
// 保存作用域
let context = this;
// 保存参数,例如 event 对象
let args = arguments;
clearTimeout(timerId);
timerId = setTimeout(function() {
callback.apply(context, args);
}, wait);
}
// 返回一个闭包
return debounced;
}
// test
let resizeFun = function(e) {
console.log('resize');
};
window.addEventListener('resize', debounce(resizeFun, 500));
复制代码
throttle
throttle
相对于 debounce
的最大区别就是它不会取消
上一次函数的执行。
所以我们可以基于 debounce
去调整一下。
function throttle(fn, wait) {
let callback = fn;
let timerId = null;
// 是否是第一次执行
let firstInvoke = true;
function throttled() {
let context = this;
let args = arguments;
// 如果是第一次触发,直接执行
if (firstInvoke) {
callback.apply(context, args);
firstInvoke = false;
return ;
}
// 如果定时器已存在,直接返回。
if (timerId) {
return ;
}
timerId = setTimeout(function() {
// 注意这里 将 clearTimeout 放到 内部来执行了
clearTimeout(timerId);
timerId = null;
callback.apply(context, args);
}, wait);
}
// 返回一个闭包
return throttled;
}
// test
let resizeFun = function(e) {
console.log('resize');
};
window.addEventListener('resize', throttle(resizeFun, 500));
复制代码
分析 lodash 中的 debounce
function debounce(func, wait, options) {
let lastArgs, // 存储 func 函数执行时的参数, 执行 debounced 函数的时候,被赋值
lastThis, // 存储 func 函数执行时的作用域, 执行 debounced 函数的时候,被赋值
maxWait, // 最长等待时间
result, // 存储 func 函数的返回值
timerId, // 定时器 id
lastCallTime // 最近一次 执行 debounced 函数时的时间
// 最近一次执行 func 时的时间戳
// 正常情况下,lastCallTime 与 lastInvokeTime 是相差无几的。
let lastInvokeTime = 0
// 是否 在延迟开始前 调用函数 - 它的作用,类似我上面实现的 throttle 方法中的 firstInvoke
let leading = false
// options 是否 传入了 maxWait
let maxing = false
// 是否 在延迟结束后 调用函数
let trailing = true
// 可以看到, debounce 函数,默认是 leading = false, trailing = true。也就意味着,默认在延迟结束后调用 func 函数
// 如果 没有传入 wait, 且 wait 不等于 0, 且浏览器支持 requestAnimationFrame时
// useRAF 会等于 true, 表示启用 requestAnimationFrame
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
// 如果 没有传入 func, 直接抛出错误
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
// 设置 wait 的默认值为 0
wait = +wait || 0
// 初始化 options 选项的 默认参数
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
// 处理 maxWait 参数,如果用户自己定义了 maxWait, 则和 wait 参数比较,取他们的最大值
// 这里是为了防止,用户 传入的 wait 大于 maxWait 的情况
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 封装执行函数, 用于立即执行 func
function invokeFunc(time) {
// 参数
const args = lastArgs
// 作用域
const thisArg = lastThis
// 重置
lastArgs = lastThis = undefined
// 记录 func 函数执行时的时间戳
lastInvokeTime = time
// 执行函数
result = func.apply(thisArg, args)
return result
}
// 开启定时器
function startTimer(pendingFunc, wait) {
if (useRAF) {
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
// 清除定时器
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
// 在延迟开始前调用
// 对 invokeFunc 的封装,返回 invokeFunc 函数的返回值
function leadingEdge(time) {
// 记录 函数被调用时 的时间戳
lastInvokeTime = time
// 开启一个定时器
timerId = startTimer(timerExpired, wait)
// 如果 leading 为 true,则表示需要在延迟开始前 先执行一次 func 函数
return leading ? invokeFunc(time) : result
}
// 在延迟结束后调用
// 对 invokeFunc 的封装,返回 invokeFunc 函数的返回值
function trailingEdge(time) {
timerId = undefined
// 这里加了个 lastArgs 的判断,lastArgs 会在 debounced 函数执行时赋值
if (trailing && lastArgs) {
return invokeFunc(time)
}
// 重置 参数和 作用域
lastArgs = lastThis = undefined
return result
}
//
function remainingWait(time) {
// 计算 time 与最近一次调用 debounced 函数的时间差
const timeSinceLastCall = time - lastCallTime
// 计算 time 与最近一次调用 func 函数的时间差
const timeSinceLastInvoke = time - lastInvokeTime
// 用 wait 减去已经等待的时间
const timeWaiting = wait - timeSinceLastCall
return maxing
// 如果设置了最大等待时长,
// 则需要 比较 ( wait 减去已经等待的时间 ) 和 ( maxWait 减去已经等待的时间 ),取最小值
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
// 否则直接返回 wait 减去已经等待的时间
: timeWaiting
}
// 根据时间判断是否可以执行函数
function shouldInvoke(time) {
// 计算 time 与最近一次调用 debounced 函数的时间差
const timeSinceLastCall = time - lastCallTime
// 计算 time 与最近一次调用 func 函数的时间差
const timeSinceLastInvoke = time - lastInvokeTime
return (
// 判断是不是第一次执行 debouned 函数,如果是第一次执行,肯定可以调用 func 函数
lastCallTime === undefined
// 判断距离最近一次调用 debounced 函数的时间差,是否大于等于 wait,如果是的话,也就意味着可以调用 func 函数
|| (timeSinceLastCall >= wait)
// 正常情况 timeSinceLastCall 不会小于 0, 除非手动调整了系统时间
|| (timeSinceLastCall < 0)
// 如果设置了 maxWait,判断距离上一次调用 func 函数的时间差,是否超过了最大等待时长
|| (maxing && timeSinceLastInvoke >= maxWait))
}
// 封装执行函数,用于 wait 延迟结束后执行
function timerExpired() {
const time = Date.now()
// 根据时间来判断是否可以执行 func 函数
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 重新计算时间,重新建一个定时器
timerId = startTimer(timerExpired, remainingWait(time))
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
// 如果可以执行 func。第一次执行的时候,isInvoking 肯定是 true
if (isInvoking) {
// 第一次执行时
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
// 如果可以执行 func,且又有 timerId 。说明 func 可以执行了,可是又没有执行 trailingEdge
// 如果又已经设置了 maxWait,就立即执行 func
// 什么时候会出现这种情况, 我还不明白。
if (maxing) {
// 开启一个定时器
timerId = startTimer(timerExpired, wait)
// 立即执行 func 函数
// 为什么设置 maxWait 就需要理解执行 func ,下面分析 throttle 的时候就明白了
return invokeFunc(lastCallTime)
}
}
// 什么情况下 不是第一次执行, 却又没有 timerId 呢?
// 因为 trailingEdge 函数内部会执行 timerId = undefined
// 如果刚好 trailingEdge 函数执行之后,又触发了 debounced ,就会出现这种情况
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
// 返回一个闭包
return debounced
}
复制代码
注意:
lastCallTime
存储的是 debounced
函数执行时的时间戳。
lastInvokeTime
存储的是 func
函数执行时的时间戳。
lodash
的 debounced
还提供了3个api
,供外部调用
// 取消 debounce
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
// 执行 func
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
// 判断是否正在等待中
function pending() {
return timerId !== undefined
}
// 暴露出三个方法
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
复制代码
分析 lodash 中的 throttle
throttle
其实就是设置了 leading
和 maxWait
的 debounce
。
function throttle(func, wait, options) {
let leading = true
let trailing = true
if (typeof func != 'function') {
throw new TypeError('Expected a function')
}
// 初始化 leading 和 trailing 的默认值
if (isObject(options)) {
leading = 'leading' in options ? !!options.leading : leading
trailing = 'trailing' in options ? !!options.trailing : trailing
}
// 默认情况下,leading 为 true, trailing 为 true。
// 表示 在延迟开始前,和延迟结束后,都需要调用 func
// 还传入了一个 maxWait
return debounce(func, wait, {
'leading': leading,
'maxWait': wait,
'trailing': trailing
})
}
复制代码
debounce
的源码,我没有完全看明白,有几个地方是我的猜测。比如 debounced
函数内的 if (maxing)
,不明白为什么要这样判断。
非常希望有明白的道友,告诉下我。
如果有错误的地方,还请指出。
谢谢阅读。