# debounce
# Description
创建一个 debounced(防抖动)函数,该函数会从上一次被调用后,延迟 wait 毫秒后调用 func 方法。 debounced(防抖动)函数提供一个 cancel 方法取消延迟的函数调用以及 flush 方法立即调用。 可以提供一个 options(选项) 对象决定如何调用 func 方法,options.leading 与 | 或 options.trailing 决定延迟前后如何触发(注:是 先调用后等待 还是 先等待后调用)。 func 调用时会传入最后一次提供给 debounced(防抖动)函数 的参数。 后续调用的 debounced(防抖动)函数返回是最后一次 func 调用的结果。
在事件被触发 n 秒后再执行回调,如果在这 n 秒内又被触发,则重新计时
# Params
(func, wait, options)
func (Function): 要防抖动的函数。
[wait=0] (number): 需要延迟的毫秒数。
[options=] (Object): 选项对象。
[options.leading=false] (boolean): 指定在延迟开始前调用。
[options.maxWait] (number): 设置 func 允许被延迟的最大值。
[options.trailing=true] (boolean): 指定在延迟结束后调用。
# Return
(Function): 返回新的 debounced(防抖动)函数。
# Depend
import isObject from './isObject.js'
import root from './.internal/root.js'
# Code
function debounce(func, wait, options) {
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const 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.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
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) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
function pending() {
return timerId !== undefined
}
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
debounced.cancel = cancel
debounced.flush = flush
debounced.pending = pending
return debounced
}
# Analyze
# 变量及其他
let lastArgs,
lastThis,
maxWait,
result,
timerId,
lastCallTime
let lastInvokeTime = 0
let leading = false
let maxing = false
let trailing = true
// Bypass `requestAnimationFrame` by explicitly setting `wait=0`.
const useRAF = (!wait && wait !== 0 && typeof root.requestAnimationFrame === 'function')
lastCallTime上次执行debounced函数的时间lastInvokeTime上一次调用func的时间timerIdsetTimeout或requestAnimationFrame返回的idmaxWait设置func允许被延迟的最大值maxing表示要不要开启 最大等待时间trailing指定在延迟结束后调用leading指定在延迟开始前调用useRAF是否使用 RAF ,如果没设置wait且 RAF 可用 则默认使用 RAF
# 参数合规化处理
if (typeof func !== 'function') {
throw new TypeError('Expected a function')
}
wait = +wait || 0
if (isObject(options)) {
leading = !!options.leading
maxing = 'maxWait' in options
maxWait = maxing ? Math.max(+options.maxWait || 0, wait) : maxWait
trailing = 'trailing' in options ? !!options.trailing : trailing
}
如果传入的
func不是一个function,则抛出 类型错误传入的
wait通过 一元正号 进行转换,如果能转成数字,则使用wait的值,否则可能为NaN, 则 取 0对于传入的
options配置进行处理leading使用双非转为Boolean值- 通过
in运算符来判断options及其原型链中是否存在maxWait属性,如果有,则开启 最大等待时间 - 如果
maxing为真值,则取maxWait和wait中最大值,因为传入的maxWait有可能会小于wait,这里和wait的处理一致,也是使用了 一元正号转换,如果maxing为假值,则maxWait还是undefined trailing也是通过in判断了是否存在,然后通过双非进行了Boolean的转换
# invokeFunc
function invokeFunc(time) {
const args = lastArgs
const thisArg = lastThis
lastArgs = lastThis = undefined
lastInvokeTime = time
result = func.apply(thisArg, args)
return result
}
invokeFunc 方法主要作用是 执行 func ,并且更新 上一次执行 func 的时间,并且会对 lastArgs 和 lastThis 进行重置
# startTimer
function startTimer(pendingFunc, wait) {
if (useRAF) {
root.cancelAnimationFrame(timerId)
return root.requestAnimationFrame(pendingFunc)
}
return setTimeout(pendingFunc, wait)
}
startTimer 作用是启用定时器,并且将等待调用的方法作为参数传递。
这里就是判断了是使用 setTimeout 还是 requestAnimationFrame
这里会返回 timerId 用作取消使用
# cancelTimer
function cancelTimer(id) {
if (useRAF) {
return root.cancelAnimationFrame(id)
}
clearTimeout(id)
}
cancelTimer 用于取消 timer,和 startTimer 一样也会区分 setTimeout 和 requestAnimationFrame
# leadingEdge
function leadingEdge(time) {
// Reset any `maxWait` timer.
lastInvokeTime = time
// Start the timer for the trailing edge.
timerId = startTimer(timerExpired, wait)
// Invoke the leading edge.
return leading ? invokeFunc(time) : result
}
leadingEdge 是在 每轮 debounce 开始时调用,会记录 上一次调用 func 的时间,第二步则是 调用 timerExpired 来进行 timer 的重启
最后会判断 如果 leading(指定在延迟开始前调用) 为真,则会立即调用 invokeFunc 函数,不会等到 timer 到时间,也就是指定在延迟开始前调用
# remainingWait
function remainingWait(time) {
const timeSinceLastCall = time - lastCallTime
const timeSinceLastInvoke = time - lastInvokeTime
const timeWaiting = wait - timeSinceLastCall
return maxing
? Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
: timeWaiting
}
remainingWait 的作用是 计算剩余时间
wait - timeSinceLastCall 可以计算出没有 maxWait 的时候的等待时间
maxWait - timeSinceLastInvoke 得出最大的等待时间所剩余的时间
然后 取 二者最小值即可
# shouldInvoke
function shouldInvoke(time) {
const timeSinceLastCall = time - lastCallTime
const 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.
return (lastCallTime === undefined || (timeSinceLastCall >= wait) ||
(timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait))
}
shouldInvoke 作用是 判断是否需要执行,拿到时间差之后会进行判断,4种情况会返回 true
第一次调用
lastCallTime === undefined距离上次调用
debounced的时间 大于等于wait的时间系统时间倒退
设置了
maxWait,距离上次调用func时间 大于等于maxWait
# timerExpired
function timerExpired() {
const time = Date.now()
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// Restart the timer.
timerId = startTimer(timerExpired, remainingWait(time))
}
timerExpired 作用是 进行 timer 的重启
通过 shouldInvoke 函数判断是否执行,传入的是当前的时间戳
如果执行,则调用 trailingEdge 函数
如果不执行,则使用 remainingWait 函数重新计算等待时间,再次调用 startTimer ,开始 timer
# trailingEdge
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
}
trailingEdge 是在计时器结束后调用,只有在 trailing 为 true 且 lastArgs 不为 undefined 时调用 invokeFunc 函数,因为 trailing 参数的缘故,有可能不会重置 lastArgs 和 lastThis ,所以这里手动重置了
# cancel
function cancel() {
if (timerId !== undefined) {
cancelTimer(timerId)
}
lastInvokeTime = 0
lastArgs = lastCallTime = lastThis = timerId = undefined
}
cancel 方法则是用于取消 timer,并且会对变量等进行重置
# flush
function flush() {
return timerId === undefined ? result : trailingEdge(Date.now())
}
flush 可以控制 func 是否立即执行,不需要等待 timer 时间到后再触发
如果 timerId 已经为 undefined,则表示 func 已经执行,或者第一次还没有调用,直接返回 result,否则调用 trailingEdge 立即执行即可
# pending
function pending() {
return timerId !== undefined
}
pending 用来检测 timer 是否正在运行,存在 timerId 则表示正在运行
# debounced
function debounced(...args) {
const time = Date.now()
const isInvoking = shouldInvoke(time)
lastArgs = args
lastThis = this
lastCallTime = time
if (isInvoking) {
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
if (maxing) {
// Handle invocations in a tight loop.
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
}
if (timerId === undefined) {
timerId = startTimer(timerExpired, wait)
}
return result
}
首先拿到当前时间以及判断是否要调用,定义了参数,
this等等如果
isInvoking为true,并且timerId为undefined,则会调用leadingEdge函数,这里会处理开启定时器,记录上一次调用func的时间,以及判断是否在延迟开始前调用如果
isInvoking为false(调用 trailingEdge 之后,在执行debounced可能会碰到shouldInvoke返回false的情况),那么则调用startTimer重新开始 timer
# if(maxing)
一开始我没看明白 if (maxing) 里面这段代码的作用,按理说,是不会执行这段代码的,后来我去 lodash 的仓库里看了 test 文件,发现对这段代码,专门有一个 case 对其测试。我剥除了一些代码,并修改了测试用例以便展示,如下
var limit = 320,
withCount = 0;
var withMaxWait = debounce(
function() {
console.log("invoke");
withCount++;
},
64,
{
maxWait: 128
}
);
var start = +new Date();
while (new Date() - start < limit) {
withMaxWait();
}
执行代码,打印了 3 次 invoke;我又将 if (maxing){} 这段代码注释,再执行代码,结果只打印了 1 次。结合源码的英文注释 Handle invocations in a tight loop,我们不难理解,原本理想的执行顺序是 withMaxWait->timer->withMaxWait->timer 这种交替进行,但由于 setTimeout 需等待主线程的代码执行完毕,所以这种短时间快速调用就会导致 withMaxWait->withMaxWait->timer->timer,从第二个 timer 开始,由于 lastArgs 被置为 undefined,也就不会再调用 invokeFunc 函数,所以只会打印一次 invoke。
同时,由于每次执行 invokeFunc 时都会将 lastArgs 置为 undefined,在执行 trailingEdge 时会对 lastArgs 进行判断,确保不会出现执行了 if (maxing){} 中的 invokeFunc 函数又执行了 timer 的 invokeFunc 函数
这段代码保证了设置 maxWait 参数后的正确性和时效性
摘自 探究防抖 (debounce) 和节流 (throttle) (opens new window)
# 总结
debounce 最终返回的是 debounced 函数,debounced 函数可以多次调用,这里在每次调用时都会更新 lastCallTime 时间,也就是调用 debounced 的时间,同时对于 this 和参数等会做缓存
每次调用都会先判断能不能调用函数 (shouldInvoke),如果 可以调用,并且现在也不是处于 timer (timerId === undefined) 阶段 会调用 leadingEdge 函数
leadingEdge 函数则会记录调用 func 的时间,然后开始定时器的启动 startTimer,会判断用户是否指定在延迟开始前调用,如果是直接调用 invokeFunc ,否则 返回上一次的结果就行
在 leadingEdge 调用 startTimer 时,传入的方法是 timerExpired
首先 startTimer ,就是判断使用 RAF 还是 setTimeout ,然后对应调用即可
也就是说 在 延迟 wait 时间后,会调用 timerExpired 方法, 然后判断 是否可以调用函数了,如果可以调用 则调用 trailingEdge , 否则 重置 timer ,这里重置 timer 时,延时时间要重新进行计算
trailingEdge 方法 则会判断是否在到时间后调用方法,然后处理一些逻辑后,调用 invokeFunc , 否则就返回上一次执行的结果
invokeFunc 则会拿到一开始缓存的 this 和参数,然后调用 func ,赋值给 结果,并返回,重置一下 this 和 参数
不考虑边界情况,基本逻辑就是这样
# Remark
setTimeout MDN (opens new window) 方法设置一个定时器,该定时器在定时器到期后执行一个函数或指定的一段代码。
返回值
timeoutID 是一个正整数,表示定时器的编号。这个值可以传递给 clearTimeout()来取消该定时器。
requestAnimationFrame MDN (opens new window) 告诉浏览器 —— 你希望执行一个动画,并且要求浏览器在下次重绘之前调用指定的回调函数更新动画。该方法需要传入一个回调函数作为参数,该回调函数会在浏览器下一次重绘之前执行
返回值
一个 long 整数,请求 ID ,是回调列表中唯一的标识。是个非零值,没别的意义。你可以传这个值给 window.cancelAnimationFrame() 以取消回调函数。
in MDN (opens new window) 如果指定的属性在指定的对象或其原型链中,则in 运算符返回 true。
# Example
const f = debounce(() => {console.log('debounce')}, 300)
const start = +new Date()
while (+new Date() - start < 200) {
console.log(1)
f()
}
/**
* 1
* 1
* ...
* debounce
*/