更新:谢谢你们的支持,最近折腾了一个博客官网出来,方便你们系统阅读,后续会有更多内容和更多优化,猛戳这里查看前端
------ 如下是正文 ------git
上一节咱们学习了 Lodash 中防抖和节流函数是如何实现的,并对源码浅析一二,今天这篇文章会经过七个小例子为切入点,换种方式继续解读源码。其中源码解析上篇文章已经很是详细介绍了,这里就再也不重复,建议本文配合上文一块儿服用,猛戳这里学习github
有什么想法或者意见均可以在评论区留言,欢迎你们拍砖。面试
咱们先来看一张图,这张图充分说明了 Throttle(节流)和 Debounce(防抖)的区别,以及在不一样配置下产生的不一样效果,其中 mousemove
事件每 50 ms 触发一次,即下图中的每一小隔是 50 ms。今天这篇文章就从下面这张图开始介绍。segmentfault
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
这部分。app
先打个预防针,后面即将开始比较难的部分,看下 debounce 入口函数。函数
// 入口函数,返回此函数
function debounced(...args) {
// 获取当前时间
const time = Date.now()
// 判断此时是否应该执行 func 函数
const isInvoking = shouldInvoke(time)
// 赋值给闭包,用于其余函数调用
lastArgs = args
lastThis = this
lastCallTime = time
// 执行
if (isInvoking) {
// 无 timerId 的状况有两种:
// 一、首次调用
// 二、trailingEdge 执行过函数
if (timerId === undefined) {
return leadingEdge(lastCallTime)
}
// 若是设置了最大等待时间,则当即执行 func
// 一、开启定时器,到时间后触发 trailingEdge 这个函数。
// 二、执行 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
,因此返回 truelastCallTime = time
,因此 lastCallTime
等于当前时间,假设为 0timerId === undefined
知足,执行 leadingEdge(lastCallTime)
方法// 执行连续事件刚开始的那次回调
function leadingEdge(time) {
// 一、设置上一次执行 func 的时间
lastInvokeTime = time
// 二、开启定时器,为了事件结束后的那次回调
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 执行传入函数 func
// leading 来源自 !!options.leading
return leading ? invokeFunc(time) : result
}
复制代码
leadingEdge(time)
中,设置 lastInvokeTime
为当前时间即 0,开启 200 毫秒定时器,执行 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
}
复制代码
invokeFunc(time)
中,执行 func.apply(thisArg, args)
,即 fn 函数第一次执行,并把结果赋值给 result
,便于后续触发时直接返回。同时重置 lastInvokeTime
为当前时间即 0,清空 lastArgs
和 lastThis
。lastCallTime
和 lastInvokeTime
都为 0,200 毫秒的定时器还在运行中。50 毫秒后第二次触发到来,此时当前时间 time
为 50,wait
为 200, maxWait
为 200,maxing
为 true,lastCallTime
和 lastInvokeTime
都为 0,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) )
}
复制代码
shouldInvoke(time)
中,timeSinceLastCall
为 50,timeSinceLastInvoke
为 50,4 种条件都不知足,返回 false。isInvoking
为 false,同时 timerId === undefined
不知足,直接返回第一次触发时的 result
result
距第一次触发 200 毫秒后第五次触发到来,此时当前时间 time
为 200,wait
为 200, maxWait
为 200,maxing
为 true,lastCallTime
为 150, lastInvokeTime
为 0,timerId
定时器存在,咱们来看下执行步骤。
shouldInvoke(time)
中,timeSinceLastInvoke
为 200,知足(maxing && timeSinceLastInvoke >= maxWait)
,因此返回 true// debounced 方法中执行到这部分
if (maxing) {
// 循环定时器中处理调用
timerId = startTimer(timerExpired, wait)
return invokeFunc(lastCallTime)
}
复制代码
maxing
条件,从新开启 200 毫秒的定时器,并执行 invokeFunc(lastCallTime)
函数invokeFunc(time)
中,重置 lastInvokeTime
为当前时间即 200,清空 lastArgs
和 lastThis
假设第八次触发以后就中止了滚动,在第八次触发时 time
为 350,因此若是有第九次触发,那么此时是应该执行fn 的,可是此时 mousemove 已经中止了触发,那么还会执行 fn 吗?答案是依旧执行,由于最开始设置了 {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
函数,来看下具体的代码。
// 定时器回调函数,表示定时结束后的操做
function timerExpired() {
const time = Date.now()
// 一、是否须要执行
// 执行事件结束后的那次回调,不然重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 二、不然 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发
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。
这里须要说明如下两点
{trailing: false}
,那么最后一次是不会执行的。对于 throttle
和 debounce
来讲,默认值是 true,因此若是没有特地指定 trailing
,那么最后一次是必定会执行的。lastArgs
来讲,执行 debounced
时会赋值,即每次触发都会从新赋值一次,那何时清空呢,在 invokeFunc(time)
中执行 fn 函数时重置为 undefined
,因此若是 debounced
只触发了一次,即便设置了 {trailing: true}
那也不会再执行 fn 函数,这个就解答了上篇文章留下的第一道思考题。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)
方法就能够了。
// 执行连续事件刚开始的那次回调
function leadingEdge(time) {
// 一、设置上一次执行 func 的时间
lastInvokeTime = time
// 二、开启定时器,为了事件结束后的那次回调
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 执行传入函数 func
// leading 来源自 !!options.leading
return leading ? invokeFunc(time) : result
}
复制代码
在这里,会开启 200 毫秒的定时器,同时由于 leading
为 false,因此并不会执行 invokeFunc(time)
,只会返回 result
,此时的 result
值是 undefined
。
这里开启一个定时器的目的是为了事件结束后的那次回调,即若是设置了 {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
值,因此具体触发过程当中的判断就不同了,来详细看一遍。
debounced
中,执行 shouldInvoke(time)
,前面讨论过由于第一次触发因此会返回 true,以后执行 leadingEdge(lastCallTime)
。// 执行连续事件刚开始的那次回调
function leadingEdge(time) {
// 一、设置上一次执行 func 的时间
lastInvokeTime = time
// 二、开启定时器,为了事件结束后的那次回调
timerId = startTimer(timerExpired, wait)
// 三、若是配置了 leading 执行传入函数 func
// leading 来源自 !!options.leading
return leading ? invokeFunc(time) : result
}
复制代码
leadingEdge
中,由于 leading
为 false,因此并不执行 fn,只开启 200 毫秒的定时器,并返回 undefined
。此时 lastInvokeTime
为当前时间,假设为 0。// 判断此时是否应该执行 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) )
}
复制代码
timeSinceLastCall
老是为 50 毫秒,maxing
为 false,因此 shouldInvoke(time)
老是返回 false,并不会执行传入函数 fn,只返回 result,即为 undefined
。timerExpired
函数// 定时器回调函数,表示定时结束后的操做
function timerExpired() {
const time = Date.now()
// 一、是否须要执行
// 执行事件结束后的那次回调,不然重启定时器
if (shouldInvoke(time)) {
return trailingEdge(time)
}
// 二、不然 计算剩余等待时间,重启定时器,保证下一次时延的末尾触发
timerId = startTimer(timerExpired, remainingWait(time))
}
复制代码
mousemove
事件一直在触发,根据前面介绍 shouldInvoke(time)
会返回 false,以后就将计算剩余等待时间,重启定时器。时间计算公式为 wait - (time - lastCallTime)
,即 200 - 50,因此只要 shouldInvoke(time)
返回 false,就每隔 150 毫秒后执行一次 timerExpired()
。mousemove
事件再也不触发,由于 timerExpired()
在循环执行,因此确定会存在一种状况知足 timeSinceLastCall >= wait
,即 shouldInvoke(time)
返回 true,终结 timerExpired()
的循环,并执行 trailingEdge(time)
。// 执行连续事件结束后的那次回调
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
中 trailing
和 lastArgs
都是 true,因此会执行 invokeFunc(time)
,即执行传入函数 fn。lodash.debounce(fn, 200, {leading: true, trailing: false})
此时相比角度 4 来讲,差别在于 {leading: true, trailing: false}
,可是 wait
和 maxWait
都和角度 4 一致,因此只存在下面 2 种区别,效果同上面第一张图所示。
leadingEdge
中会执行传入函数 fntrailingEdge
中再也不执行传入函数 fnlodash.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。
lodash.debounce(fn, 200, {leading: false, trailing: true, maxWait: 400})
此时 wait
为 200,maxWait
为 400,maxing
为 true,咱们来看下执行过程。
{leading: false}
,因此确定不会执行 fn,此时开启了一个 200 毫秒的定时器。// 判断此时是否应该执行 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) )
}
复制代码
shouldInvoke(time)
函数,只有在第 400 毫秒时,才会知足 maxing && timeSinceLastInvoke >= maxWait
,返回 true。// 计算仍需等待的时间
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
}
复制代码
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
函数能够接受参数,因此能够用高阶函数的方式传参,以下
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);
复制代码
若是你以为这篇内容对你挺有启发,我想邀请你帮我三个小忙: