We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
上一节我们学习了 Lodash 中防抖和节流函数是如何实现的,并对源码浅析一二,今天这篇文章会通过七个小例子为切入点,换种方式继续解读源码。其中源码解析上篇文章已经非常详细介绍了,这里就不再重复,建议本文配合上文一起服用,猛戳这里学习
有什么想法或者意见都可以在评论区留言,欢迎大家拍砖。
我们先来看一张图,这张图充分说明了 Throttle(节流)和 Debounce(防抖)的区别,以及在不同配置下产生的不同效果,其中 mousemove 事件每 50 ms 触发一次,即下图中的每一小隔是 50 ms。今天这篇文章就从下面这张图开始介绍。
mousemove
lodash.throttle(fn, 200, {leading: true, trailing: true})
先来看下 throttle 源码
function throttle(func, wait, options) { // 首尾调用默认为 true let leading = true let trailing = true if (typeof func !== 'function') { throw new TypeError('Expected a function') } // options 是否是对象 if (isObject(options)) { leading = 'leading' in options ? !!options.leading : leading trailing = 'trailing' in options ? !!options.trailing : trailing } // maxWait 为 wait 的防抖函数 return debounce(func, wait, { leading, trailing, 'maxWait': wait, }) }
所以 throttle(fn, 200, {leading: true, trailing: true}) 返回内容是 debounce(fn, 200, {leading: true, trailing: true, maxWait: 200}),多了 maxWait: 200 这部分。
throttle(fn, 200, {leading: true, trailing: true})
debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
maxWait: 200
先打个预防针,后面即将开始比较难的部分,看下 debounce 入口函数。
// 入口函数,返回此函数 function debounced(...args) { // 获取当前时间 const time = Date.now() // 判断此时是否应该执行 func 函数 const isInvoking = shouldInvoke(time) // 赋值给闭包,用于其他函数调用 lastArgs = args lastThis = this lastCallTime = time // 执行 if (isInvoking) { // 无 timerId 的情况有两种: // 1、首次调用 // 2、trailingEdge 执行过函数 if (timerId === undefined) { return leadingEdge(lastCallTime) } // 如果设置了最大等待时间,则立即执行 func // 1、开启定时器,到时间后触发 trailingEdge 这个函数。 // 2、执行 func,并返回结果 if (maxing) { // 循环定时器中处理调用 timerId = startTimer(timerExpired, wait) return invokeFunc(lastCallTime) } } // 一种特殊情况,trailing 设置为 true 时,前一个 wait 的 trailingEdge 已经执行了函数 // 此时函数被调用时 shouldInvoke 返回 false,所以要开启定时器 if (timerId === undefined) { timerId = startTimer(timerExpired, wait) } // 不需要执行时,返回结果 return result }
对于 debounce(fn, 200, {leading: true, trailing: true, maxWait: 200}) 来说,会经历如下过程。
shouldInvoke(time)
lastCallTime === undefined
lastCallTime = time
lastCallTime
timerId === undefined
leadingEdge(lastCallTime)
// 执行连续事件刚开始的那次回调 function leadingEdge(time) { // 1、设置上一次执行 func 的时间 lastInvokeTime = time // 2、开启定时器,为了事件结束后的那次回调 timerId = startTimer(timerExpired, wait) // 3、如果配置了 leading 执行传入函数 func // leading 来源自 !!options.leading return leading ? invokeFunc(time) : result }
leadingEdge(time)
lastInvokeTime
invokeFunc(time)
// 执行 Func 函数 function invokeFunc(time) { // 获取上一次执行 debounced 的参数 const args = lastArgs // 获取上一次的 this const thisArg = lastThis // 重置 lastArgs = lastThis = undefined lastInvokeTime = time result = func.apply(thisArg, args) return result }
func.apply(thisArg, args)
result
lastArgs
lastThis
50 毫秒后第二次触发到来,此时当前时间 time 为 50,wait 为 200, maxWait 为 200,maxing 为 true,lastCallTime 和 lastInvokeTime 都为 0,timerId 定时器存在,我们来看下执行步骤。
time
wait
maxWait
maxing
timerId
function shouldInvoke(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime // 下述 4 种情况返回 true return ( lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait) ) }
timeSinceLastCall
timeSinceLastInvoke
isInvoking
距第一次触发 200 毫秒后第五次触发到来,此时当前时间 time 为 200,wait 为 200, maxWait 为 200,maxing 为 true,lastCallTime 为 150, lastInvokeTime 为 0,timerId 定时器存在,我们来看下执行步骤。
(maxing && timeSinceLastInvoke >= maxWait)
// debounced 方法中执行到这部分 if (maxing) { // 循环定时器中处理调用 timerId = startTimer(timerExpired, wait) return invokeFunc(lastCallTime) }
invokeFunc(lastCallTime)
假设第八次触发之后就停止了滚动,在第八次触发时 time 为 350,所以如果有第九次触发,那么此时是应该执行fn 的,但是此时 mousemove 已经停止了触发,那么还会执行 fn 吗?答案是依旧执行,因为最开始设置了 {trailing: true}。
{trailing: true}
// 开启定时器 function startTimer(pendingFunc, wait) { // 没传 wait 时调用 window.requestAnimationFrame() if (useRAF) { // 若想在浏览器下次重绘之前继续更新下一帧动画 // 那么回调函数自身必须再次调用 window.requestAnimationFrame() root.cancelAnimationFrame(timerId); return root.requestAnimationFrame(pendingFunc) } // 不使用 RAF 时开启定时器 return setTimeout(pendingFunc, wait) }
在第五次触发时开启了 200 毫秒的定时器,所以在时间 time 到 400 时会执行 pendingFunc,此时的 pendingFunc 就是 timerExpired 函数,来看下具体的代码。
pendingFunc
timerExpired
// 定时器回调函数,表示定时结束后的操作 function timerExpired() { const time = Date.now() // 1、是否需要执行 // 执行事件结束后的那次回调,否则重启定时器 if (shouldInvoke(time)) { return trailingEdge(time) } // 2、否则 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发 timerId = startTimer(timerExpired, remainingWait(time)) }
此时在 shouldInvoke(time) 中,time 为 400,lastInvokeTime 为 200,timeSinceLastInvoke 为 200,满足 (maxing && timeSinceLastInvoke >= maxWait),所以返回 true。
// 执行连续事件结束后的那次回调 function trailingEdge(time) { // 清空定时器 timerId = undefined // trailing 和 lastArgs 两者同时存在时执行 // trailing 来源自 'trailing' in options ? !!options.trailing : trailing // lastArgs 标记位的作用,意味着 debounce 至少执行过一次 if (trailing && lastArgs) { return invokeFunc(time) } // 清空参数 lastArgs = lastThis = undefined return result }
之后执行 trailingEdge(time),在这个函数中判断 trailing 和 lastArgs ,此时这两个条件都是 true,所以会执行 invokeFunc(time),最终执行函数 fn。
trailingEdge(time)
trailing
这里需要说明以下两点
{trailing: false}
throttle
debounce
debounced
undefined
lodash.throttle(fn, 200, {leading: true, trailing: false})
在「角度 1 之 mousemove 停止触发」这部分中说到,如果不设置 trailing 和设置 {trailing: true} 效果是一样的,事件回调结束后都会再执行一次传入函数 fn,但是如果设置了{trailing: false},那么事件回调结束后是不会再执行 fn 的。
此时的配置对比角度 1 来说,区别在于设置了{trailing: false},所以实际效果对比 1 来说,就是最后不会额外再执行一次,效果见第一张图。
lodash.throttle(fn, 200, {leading: false, trailing: true})
此时的配置和角度 1 相比,区别在于设置了 {leading: false},所以直接看 leadingEdge(time) 方法就可以了。
{leading: false}
在这里,会开启 200 毫秒的定时器,同时因为 leading 为 false,所以并不会执行 invokeFunc(time) ,只会返回 result,此时的 result 值是 undefined。
leading
这里开启一个定时器的目的是为了事件结束后的那次回调,即如果设置了 {trailing: true} 那么最后一次回调将执行传入函数 fn,哪怕 debounced 函数只触发一次。
这里指定了 {leading: false},那么 leading 的初始值是什么呢?在 debounce 中是 false,在 throttle 中是 true。所以在 throttle 中不需要刚开始就触发时,必须指定 {leading: false},在 debounce 中就不需要了,默认不触发。
lodash.debounce(fn, 200, {leading: false, trailing: true})
此时相比较 throttle 来说,缺少了 maxWait 值,所以具体触发过程中的判断就不一样了,来详细看一遍。
leadingEdge
// 判断此时是否应该执行 func 函数 function shouldInvoke(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime // 下述 4 种情况返回 true return ( lastCallTime === undefined || (timeSinceLastCall >= wait) || (timeSinceLastCall < 0) || (maxing && timeSinceLastInvoke >= maxWait) ) }
wait - (time - lastCallTime)
timerExpired()
timeSinceLastCall >= wait
trailingEdge
lodash.debounce(fn, 200, {leading: true, trailing: false})
此时相比角度 4 来说,差异在于 {leading: true, trailing: false},但是 wait 和 maxWait 都和角度 4 一致,所以只存在下面 2 种区别,效果同上面第一张图所示。
{leading: true, trailing: false}
lodash.debounce(fn, 200, {leading: true, trailing: true})
此时相比角度 4 来说,差异仅仅在于设置了 {leading: true},所以只存在一个区别,那就是在 leadingEdge 中会执行传入函数 fn,当然在 trailingEdge 中依旧执行传入函数 fn,所以会出现在 mousemove 事件触发过程中首尾都会执行的情况,效果同上面第一张图所示。
{leading: true}
当然一种情况除外,那就是 mousemove 事件永远只触发一次的情况,关键在于 lastArgs 变量。
对于 lastArgs 变量来说,在入口函数 debounced 中赋值,即每次触发都会重新赋值一次,那什么时候清空呢,在 invokeFunc(time) 中重置为 undefined,所以如果 debounced 只触发了一次,而且在 {leading: true} 时执行过一次 fn,那么即使设置了 {trailing: true} 也不会再执行传入函数 fn。
lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})
此时 wait 为 200,maxWait 为 400,maxing 为 true,我们来看下执行过程。
maxing && timeSinceLastInvoke >= maxWait
// 计算仍需等待的时间 function remainingWait(time) { // 当前时间距离上一次调用 debounce 的时间差 const timeSinceLastCall = time - lastCallTime // 当前时间距离上一次执行 func 的时间差 const timeSinceLastInvoke = time - lastInvokeTime // 剩余等待时间 const timeWaiting = wait - timeSinceLastCall // 是否设置了最大等待时间 // 是(节流):返回「剩余等待时间」和「距上次执行 func 的剩余等待时间」中的最小值 // 否:返回剩余等待时间 return maxing ? Math.min(timeWaiting, maxWait - timeSinceLastInvoke) : timeWaiting }
timeWaiting
maxWait - timeSinceLastInvoke
Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
if (timerId === undefined) {timerId = startTimer(timerExpired, wait)}
问:如果 leading 和 trailing 选项都是 true,在 wait 期间只调用了一次 debounced 函数时,总共会调用几次 func,1 次还是 2 次,为什么?
func
答案是 1 次,为什么?文中已给出详细解答,详情请看角度 1 和角度 6。
问:如何给 debounce(func, time, options) 中的 func 传参数?
debounce(func, time, options)
第一种方案,因为 debounced 函数可以接受参数,所以可以用高阶函数的方式传参,如下
const params = 'muyiy'; const debounced = lodash.debounce(func, 200)(params) window.addEventListener('mousemove', debounced);
不过这种方式不太友好,params 会将原来的 event 覆盖掉,此时就拿不到 scroll 或者 mousemove 等事件对象 event 了。
第二种方案,在监听函数上处理,使用闭包保存传入参数并返回需要执行的函数即可。
function onMove(param) { console.log('param:', param); // muyiy function func(event) { console.log('param:', param); // muyiy console.log('event:', event); // event } return func; }
使用时如下
const params = 'muyiy'; const debounced = lodash.debounce(onMove(params), 200) window.addEventListener('mousemove', debounced);
函数防抖 (debounce) 和节流 (throttle) 以及 lodash 的 debounce 源码赏析
【进阶 6-3 期】深入浅出节流函数 throttle 【进阶 6-4 期】深入浅出防抖函数 debounce 【进阶 6-5 期】[译] Throttle 和 Debounce 在 React 中的应用 【进阶 6-6 期】深入篇 | 阿里 P6 必会 Lodash 防抖节流函数实现原理
【进阶 6-3 期】深入浅出节流函数 throttle
【进阶 6-4 期】深入浅出防抖函数 debounce
【进阶 6-5 期】[译] Throttle 和 Debounce 在 React 中的应用
【进阶 6-6 期】深入篇 | 阿里 P6 必会 Lodash 防抖节流函数实现原理
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
The text was updated successfully, but these errors were encountered:
博主怎么断更了?
Sorry, something went wrong.
No branches or pull requests
引言
上一节我们学习了 Lodash 中防抖和节流函数是如何实现的,并对源码浅析一二,今天这篇文章会通过七个小例子为切入点,换种方式继续解读源码。其中源码解析上篇文章已经非常详细介绍了,这里就不再重复,建议本文配合上文一起服用,猛戳这里学习
有什么想法或者意见都可以在评论区留言,欢迎大家拍砖。
节流函数 Throttle
我们先来看一张图,这张图充分说明了 Throttle(节流)和 Debounce(防抖)的区别,以及在不同配置下产生的不同效果,其中
mousemove
事件每 50 ms 触发一次,即下图中的每一小隔是 50 ms。今天这篇文章就从下面这张图开始介绍。角度 1
lodash.throttle(fn, 200, {leading: true, trailing: true})
mousemove 第一次触发
先来看下 throttle 源码
所以
throttle(fn, 200, {leading: true, trailing: true})
返回内容是debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
,多了maxWait: 200
这部分。先打个预防针,后面即将开始比较难的部分,看下 debounce 入口函数。
对于
debounce(fn, 200, {leading: true, trailing: true, maxWait: 200})
来说,会经历如下过程。shouldInvoke(time)
中,因为满足条件lastCallTime === undefined
,所以返回 truelastCallTime = time
,所以lastCallTime
等于当前时间,假设为 0timerId === undefined
满足,执行leadingEdge(lastCallTime)
方法leadingEdge(time)
中,设置lastInvokeTime
为当前时间即 0,开启 200 毫秒定时器,执行invokeFunc(time)
并返回invokeFunc(time)
中,执行func.apply(thisArg, args)
,即 fn 函数第一次执行,并把结果赋值给result
,便于后续触发时直接返回。同时重置lastInvokeTime
为当前时间即 0,清空lastArgs
和lastThis
。lastCallTime
和lastInvokeTime
都为 0,200 毫秒的定时器还在运行中。mousemove 第二次触发
50 毫秒后第二次触发到来,此时当前时间
time
为 50,wait
为 200,maxWait
为 200,maxing
为 true,lastCallTime
和lastInvokeTime
都为 0,timerId
定时器存在,我们来看下执行步骤。shouldInvoke(time)
中,timeSinceLastCall
为 50,timeSinceLastInvoke
为 50,4 种条件都不满足,返回 false。isInvoking
为 false,同时timerId === undefined
不满足,直接返回第一次触发时的result
result
mousemove 第五次触发
距第一次触发 200 毫秒后第五次触发到来,此时当前时间
time
为 200,wait
为 200,maxWait
为 200,maxing
为 true,lastCallTime
为 150,lastInvokeTime
为 0,timerId
定时器存在,我们来看下执行步骤。shouldInvoke(time)
中,timeSinceLastInvoke
为 200,满足(maxing && timeSinceLastInvoke >= maxWait)
,所以返回 truemaxing
条件,重新开启 200 毫秒的定时器,并执行invokeFunc(lastCallTime)
函数invokeFunc(time)
中,重置lastInvokeTime
为当前时间即 200,清空lastArgs
和lastThis
mousemove 停止触发
假设第八次触发之后就停止了滚动,在第八次触发时
time
为 350,所以如果有第九次触发,那么此时是应该执行fn 的,但是此时 mousemove 已经停止了触发,那么还会执行 fn 吗?答案是依旧执行,因为最开始设置了{trailing: true}
。在第五次触发时开启了 200 毫秒的定时器,所以在时间
time
到 400 时会执行pendingFunc
,此时的pendingFunc
就是timerExpired
函数,来看下具体的代码。此时在
shouldInvoke(time)
中,time
为 400,lastInvokeTime
为 200,timeSinceLastInvoke
为 200,满足(maxing && timeSinceLastInvoke >= maxWait)
,所以返回 true。之后执行
trailingEdge(time)
,在这个函数中判断trailing
和lastArgs
,此时这两个条件都是 true,所以会执行invokeFunc(time)
,最终执行函数 fn。这里需要说明以下两点
{trailing: false}
,那么最后一次是不会执行的。对于throttle
和debounce
来说,默认值是 true,所以如果没有特意指定trailing
,那么最后一次是一定会执行的。lastArgs
来说,执行debounced
时会赋值,即每次触发都会重新赋值一次,那什么时候清空呢,在invokeFunc(time)
中执行 fn 函数时重置为undefined
,所以如果debounced
只触发了一次,即使设置了{trailing: true}
那也不会再执行 fn 函数,这个就解答了上篇文章留下的第一道思考题。角度 2
lodash.throttle(fn, 200, {leading: true, trailing: false})
在「角度 1 之 mousemove 停止触发」这部分中说到,如果不设置
trailing
和设置{trailing: true}
效果是一样的,事件回调结束后都会再执行一次传入函数 fn,但是如果设置了{trailing: false}
,那么事件回调结束后是不会再执行 fn 的。此时的配置对比角度 1 来说,区别在于设置了
{trailing: false}
,所以实际效果对比 1 来说,就是最后不会额外再执行一次,效果见第一张图。角度 3
lodash.throttle(fn, 200, {leading: false, trailing: true})
此时的配置和角度 1 相比,区别在于设置了
{leading: false}
,所以直接看leadingEdge(time)
方法就可以了。在这里,会开启 200 毫秒的定时器,同时因为
leading
为 false,所以并不会执行invokeFunc(time)
,只会返回result
,此时的result
值是undefined
。这里开启一个定时器的目的是为了事件结束后的那次回调,即如果设置了
{trailing: true}
那么最后一次回调将执行传入函数 fn,哪怕debounced
函数只触发一次。这里指定了
{leading: false}
,那么leading
的初始值是什么呢?在debounce
中是 false,在throttle
中是 true。所以在throttle
中不需要刚开始就触发时,必须指定{leading: false}
,在debounce
中就不需要了,默认不触发。防抖函数 Debounce
角度 4
lodash.debounce(fn, 200, {leading: false, trailing: true})
此时相比较 throttle 来说,缺少了
maxWait
值,所以具体触发过程中的判断就不一样了,来详细看一遍。debounced
中,执行shouldInvoke(time)
,前面讨论过因为第一次触发所以会返回 true,之后执行leadingEdge(lastCallTime)
。leadingEdge
中,因为leading
为 false,所以并不执行 fn,只开启 200 毫秒的定时器,并返回undefined
。此时lastInvokeTime
为当前时间,假设为 0。timeSinceLastCall
总是为 50 毫秒,maxing
为 false,所以shouldInvoke(time)
总是返回 false,并不会执行传入函数 fn,只返回 result,即为undefined
。timerExpired
函数mousemove
事件一直在触发,根据前面介绍shouldInvoke(time)
会返回 false,之后就将计算剩余等待时间,重启定时器。时间计算公式为wait - (time - lastCallTime)
,即 200 - 50,所以只要shouldInvoke(time)
返回 false,就每隔 150 毫秒后执行一次timerExpired()
。mousemove
事件不再触发,因为timerExpired()
在循环执行,所以肯定会存在一种情况满足timeSinceLastCall >= wait
,即shouldInvoke(time)
返回 true,终结timerExpired()
的循环,并执行trailingEdge(time)
。trailingEdge
中trailing
和lastArgs
都是 true,所以会执行invokeFunc(time)
,即执行传入函数 fn。角度 5
lodash.debounce(fn, 200, {leading: true, trailing: false})
此时相比角度 4 来说,差异在于
{leading: true, trailing: false}
,但是wait
和maxWait
都和角度 4 一致,所以只存在下面 2 种区别,效果同上面第一张图所示。leadingEdge
中会执行传入函数 fntrailingEdge
中不再执行传入函数 fn角度 6
lodash.debounce(fn, 200, {leading: true, trailing: true})
此时相比角度 4 来说,差异仅仅在于设置了
{leading: true}
,所以只存在一个区别,那就是在leadingEdge
中会执行传入函数 fn,当然在trailingEdge
中依旧执行传入函数 fn,所以会出现在 mousemove 事件触发过程中首尾都会执行的情况,效果同上面第一张图所示。当然一种情况除外,那就是
mousemove
事件永远只触发一次的情况,关键在于lastArgs
变量。对于
lastArgs
变量来说,在入口函数debounced
中赋值,即每次触发都会重新赋值一次,那什么时候清空呢,在invokeFunc(time)
中重置为undefined
,所以如果debounced
只触发了一次,而且在{leading: true}
时执行过一次 fn,那么即使设置了{trailing: true}
也不会再执行传入函数 fn。角度 7
lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})
此时
wait
为 200,maxWait
为 400,maxing
为 true,我们来看下执行过程。{leading: false}
,所以肯定不会执行 fn,此时开启了一个 200 毫秒的定时器。shouldInvoke(time)
函数,只有在第 400 毫秒时,才会满足maxing && timeSinceLastInvoke >= maxWait
,返回 true。timerExpired
,因为此时shouldInvoke(time)
返回 false,所以会重新计算剩余等待时间并重启计时器,其中timeWaiting
是 150 毫秒,maxWait - timeSinceLastInvoke
是 200 毫秒,所以计算结果是150 毫秒。timeWaiting
依旧是 150 毫秒,maxWait - timeSinceLastInvoke
是 50 毫秒,所以重新开启 50 毫秒的定时器,即在第 400 毫秒时触发。shouldInvoke(time)
中返回 true 的时间也是在第 400 毫秒,为什么要这样呢?这样会冲突吗?首先定时器剩余时间判断和shouldInvoke(time)
判断中,只要有一处满足执行 fn 条件,就会立马执行,同时lastInvokeTime
值也会发生改变,所以另一处判断就不会生效了。另外本身定时器是不精准的,所以通过Math.min(timeWaiting, maxWait - timeSinceLastInvoke)
取最小值的方式来减少误差。if (timerId === undefined) {timerId = startTimer(timerExpired, wait)}
,避免trailingEdge
执行后定时器被清空。上期答疑
第一题
问:如果
leading
和trailing
选项都是 true,在wait
期间只调用了一次debounced
函数时,总共会调用几次func
,1 次还是 2 次,为什么?答案是 1 次,为什么?文中已给出详细解答,详情请看角度 1 和角度 6。
第二题
问:如何给
debounce(func, time, options)
中的func
传参数?第一种方案,因为
debounced
函数可以接受参数,所以可以用高阶函数的方式传参,如下不过这种方式不太友好,params 会将原来的 event 覆盖掉,此时就拿不到 scroll 或者 mousemove 等事件对象 event 了。
第二种方案,在监听函数上处理,使用闭包保存传入参数并返回需要执行的函数即可。
使用时如下
参考
推荐阅读
❤️ 看完三件事
如果你觉得这篇内容对你挺有启发,我想邀请你帮我三个小忙:
The text was updated successfully, but these errors were encountered: