[深入10] Debounce Throttle

导航 [[深入01] 执行上下文](https://juejin.im/post/684490...
[[深入02] 原型链](https://juejin.im/post/684490...
[[深入03] 继承](https://juejin.im/post/684490...
[[深入04] 事件循环](https://juejin.im/post/684490...
[[深入05] 柯里化 偏函数 函数记忆](https://juejin.im/post/684490...
[[深入06] 隐式转换 和 运算符](https://juejin.im/post/684490...
[[深入07] 浏览器缓存机制(http缓存机制)](https://juejin.im/post/684490...
[[深入08] 前端安全](https://juejin.im/post/684490...
[[深入09] 深浅拷贝](https://juejin.im/post/684490...
[[深入10] Debounce Throttle](https://juejin.im/post/684490...
[[深入11] 前端路由](https://juejin.im/post/684490...
[[深入12] 前端模块化](https://juejin.im/post/684490...
[[深入13] 观察者模式 发布订阅模式 双向数据绑定](https://juejin.im/post/684490...
[[深入14] canvas](https://juejin.im/post/684490...
[[深入15] webSocket](https://juejin.im/post/684490...
[[深入16] webpack](https://juejin.im/post/684490...
[[深入17] http 和 https](https://juejin.im/post/684490...
[[深入18] CSS-interview](https://juejin.im/post/684490...
[[深入19] 手写Promise](https://juejin.im/post/684490...
[[深入20] 手写函数](https://juejin.im/post/684490...
[[react] Hooks](https://juejin.im/post/684490...
【[深入10] Debounce Throttle】[[部署01] Nginx](https://juejin.im/post/684490...
[[部署02] Docker 部署vue项目](https://juejin.im/post/684490...
[[部署03] gitlab-CI](https://juejin.im/post/684490...
[[源码-webpack01-前置知识] AST抽象语法树](https://juejin.im/post/684490...
[[源码-webpack02-前置知识] Tapable](https://juejin.im/post/684490...
[[源码-webpack03] 手写webpack - compiler简单编译流程](https://juejin.im/post/684490...
[[源码] Redux React-Redux01](https://juejin.im/post/684490...
[[源码] axios ](https://juejin.im/post/684490...
[[源码] vuex ](https://juejin.im/post/684490...
[[源码-vue01] data响应式 和 初始化渲染 ](https://juejin.im/post/684490...
Debounce 防抖函数

  • 特点:延时执行,如果在延时的时间内多次触发,则从新计时
  • 过程:当事件A发生时,设置一个定时器,a秒后触发A的回调函数,如果在a秒内有新的同一事件发生,则清除定时器,并从新开始计时(即又在a秒后触发A的回调,注意:上次的A的回调并未触发,而是定时器被清除了,定时器中A的回调就不会被执行)
版本一 (基础版本)
  • 优点:可以传参,比如点击时,点击事件提供的 event 对象
  • 缺点:
    • 第一次触发是不需要延时的,版本一的第一次也是需要定时器的delay时间后才会执行
    • 不能手动取消debounce的执行,在delay时间未到时的最后一次的执行
    版本一 (基础版本)/** * @param {function} fn 需要debounce防抖函数处理的函数 * @param {number} delay 定时器延时的时间 */ function debounce(fn, delay) { let timer = null // 该变量常驻内存,可以记住上一次的状态 // 只有在外层函数失去引用时,该变量才会清除 // 缓存定时器idreturn (...args) => { // 返回一个闭包 // 注意参数:比如事件对象 event 能够获取到 if (timer) { // timer存在,就清除定时器 // 清除定时器,则定时器对应的回调函数也就不会执行 clearTimeout(timer) } // 清除定时器后,重新计时 timer = setTimeout(() => { fn.call(this, ...args) // this需要定时器回调函数时才能确定,this指向调用时所在的对象,大多数情况都指向window }, delay) } }

版本二 (升级版本)
  • 解决问题:解决第一次点击不能立即触发的问题
  • 解决问题:在delay时间没有到时,手动的取消debounce的执行
  • 实现的结果:
    • 第一次点击立即触发
    • 如果从第一次点击开始,一直不间断频繁点击(未超过delay时间),然后停止点击不再点击,会触发两次,第一次是立即执行的,第二次是debounce延时执行的
    • 可以手动取消debounce的执行 其实就是手动清除最后一次的timer
版本二 (升级版本)/** * @param {function} fn 需要debounce防抖函数处理的函数 * @param {number} delay 定时器延时的时间 * @param {boolean} immediate 是否立即执行 */ function debounce(fn, delay, immediate) { let timer = null return (...args) => { // 这里可以拿到事件对象 if (immediate && !timer) { // 如果立即执行标志位是 true,并且timer不存在 // 即第一次触发的情况 // 以后的触发由于timer存在,则不再进入执行 // 注意:timer是setTimeout()执行返回的值,不是setTimeout()的回调执行时才返回,是立即返回的 // 注意:所以第二次触发时,timer就已经有值了,不是setTimeout()的回调执行时才返回 fn.call(this, ...args) // 解决: // timer = 1 // return } if (timer) { clearTimeout(timer) // timer存在,就清除定时器 // 清除定时器,则定时器对应的回调函数也就不会执行 } timer = setTimeout(() => { console.log(args, 'args') console.log(this, 'this') fn.call(this, ...args) // 注意:有一个特殊情况 // 比如:只点击一次,在上面的immediate&&!timer判断中会立即执行一次,然后在delay后,定时器中也会触发一次 // 如何解决执行两次: 在上面的immediate&&!timer判断中立即执行一次fn后,将timer=1,同时return,将不再往下执行,同时timer存在// -------------------- // if (!immediate) { //fn.call(this, ...args) // } // immediate = false // 注释的操作可以只在点击一次没有再点击的情况只执行一次 // 但是:一次性多次点击,第二次不会触发,只有再停顿达到delay后,再次点击才会正常的达到debounce的效果 // --------------------}, delay)// 手动取消执行debounce函数 debounce.cancel = function () { clearTimeout(timer) } } }

版本三 (变更需求)
  • 需求:第一次立即执行,然后等到停止触发delay毫秒后,才可以重新触发
    版本三 (变更需求) 需求:第一次立即执行,然后等到停止触发delay毫秒后,才可以重新触发/** * @param {function} fn 需要debounce防抖函数处理的函数 * @param {number} delay 定时器延时的时间 * @param {boolean} immediate 是否立即执行 */ function debounce(fn, delay, immediate) { let timer return (...args) => {if (timer) { clearTimeout(timer) }if(!immediate) { // 不立即执行的情况 // 和最初的版本一样 timer = setTimeout(() => { fn.call(this, ...args) }, delay) } else { // 立即执行 const cacheTimer = timer // 缓存timer // 缓存timer, 因为下面timer会立即改变,如果直接用timer判断,fn不会执行 // 立即执行的情况下,第一次:cacheTimer => false // 立即执行的情况下,第二次:cacheTimer => true,因为直到delay毫秒后,timer才会被修改,cacheTimer 变为false timer = setTimeout(() => { timer = null // delay后,timer从新改为null,则满足条件!cacheTimer,则fn会再次执行 }, delay) if(!cacheTimer) { // 缓存了timer,所以立即执行的情况,第一次缓存的timer时false,会立即执行fn fn.call(this, ...args) } } } }


案例1
Document - 锐客网 点击

案例二 - react中
  • 手动取消
    function App() { const fn = () => { console.log('fn') } const debounce = (fn, delay, immediate) => { let timer = null return (...args) => { if (immediate && !timer) { fn.call(this, ...args) } if (timer) { clearTimeout(timer) } timer = setTimeout(() => { fn.call(this, ...args) }, delay) debounce.cancel = function () { // 手动取消debounce clearTimeout(timer) } } } const cancleDebounce = () => { debounce.cancel() } return (点击2 取消执行); }

    在真实项目中的运用
  • 如视频监听断流的回调,会不停的执行监听函数,当视频当断流时,就不再执行监听函数了,此时可以用debounce,就能处理监听到断流后需要处理的事情,比如提示断流
  • input框的查询结果,不需要输入每个字符都去查询结果,而是使用debounce函数去处理查询后端接口
  • 小结:Debounce需要考虑第一次执行,手动取消执行,事件对象event等参数的传递问题
Throttle
  • 特点:每隔一段时间,只执行一次
  • 在时间a内,只会执行一次函数,多次触发也只会触发一次
版本一(基础版本)
  • 原理:设置一个标志位为true,在闭包中判断标志位,false则turn;接着把表示为改为false,第二次就直接返回了,不会执行定时器,定时器执行完,标志位改为true,则又可以进入闭包执行定时器;同时定时器执行完,要清除定时器
    function throttle(fn, delay) { let isRun = true // 标志位 return (...args) => { if (!isRun) { // false则跳出函数,不再向下执行 return } isRun = false // 立即改为false,则下次不会再执行到定位器,直到定时器执行完,isRun为true,才有机会执行到定时器 let timer = setTimeout(() => { fn.call(this, ...args) isRun = true clearTimeout(timer) // 执行完所有操作后,清除定时器 }, delay) } }

    版本二(利用时间戳)
  • 原理:比较两次点击的时间戳差值(单位是毫秒),大于delay毫秒则执行fn
    function throttle(fn, delay) { let previous = 0 // 缓存上一次的时间戳 return (...args) => { const now = + new Date() // (+)一元加运算符:可以把任意类型的数据转换成(数值),结果只能是(数值)和(NaN)两种 // 获取现在的时间戳,即距离1970.1.1 00:00:00的毫秒数字 // 注意:单位是毫秒数,和定时器的第二个参数吻合,也是毫秒数 if (now - previous > delay) { // 第一次:now - previous > delay是true,所以立即执行一次 // 然后 previous = now // 第二次:第二次能进来的条件就是差值毫秒数超过delay毫秒 // 这样频繁的点击时,就能按照固定的频率执行,当然是降低了频率 fn.call(this, ...args) previous = now // 注意:执行完记得同步时间 } } }

    在真实项目中的运用
  • 浏览器窗口的resize
  • 滚动条的滚动监听函数需要触发的回调
  • 上拉加载更多
underscore中的Throttle
前置知识: - leading:是头部,领导的意思 - trailing: 是尾部的意思 - remaining:剩余的意思 (remain:剩余)options.leading=> 布尔值,表示是否执行事件刚开始的那次回调,false表示不执行开始时的回调 options.trailing => 布尔值,表示是否执行事件结束时的那次回调,false表示不执行结束时的回调_.throttle = function(func, wait, options) { // func:throttle函数触发时需要执行的函数 // wait:定时器的延迟时间 // options:配置对象,有 leading 和 trailing 属性var timeout, context, args, result; // timeout:定时器ID // context:上下文环境,用来固定this // args:传入func的参数 // result:func函数执行的返回值,因为func是可能存在返回值的,所以需要考虑到返回值的赋值var previous = 0; // 记录上一次事件触发的时间戳,用来缓存每一次的 now // 第一次是:0 // 以后就是:上一次的时间戳if (!options) options = {}; // 配置对象不存在,就设置为空对象var later = function() { // later是定时器的回调函数 previous = options.leading === false ? 0 : _.now(); timeout = null; // 重新赋值为null,用于条件判断,和下面的操作一样 result = func.apply(context, args); if (!timeout) context = args = null; // timer必然为null,上面重新赋值了,重置context, args }; var throttled = function() {var now = _.now(); // 获取当前时间的时间戳if (!previous && options.leading === false) previous = now; // 如果previous不存在,并且第一次回调不需要执行的话,previous = now // previous // 第一次是:previous = 0 // 之后都是:previous是上次的时间戳 // options.leading === false // 注意:这里是三等,即类型不一样的话都是false // 所以:leading是undefined时,undefined === false 结果是 fale,因为类型都不一样var remaining = wait - (now - previous); // remaining:表示距离下次触发 func 还需等待的时间 // remaining的值的取值情况,下面有分析context = this; // 固定this指向args = arguments; // 获取func的实参if (remaining <= 0 || remaining > wait) { // remaining <= 0 的所有情况如下: // 情况1: // 第一次触发,并且(不传options或传入的options.leading === true)即需要立即执行第一次回调 // remaining = wait - (now - 0) => remaining = wait - now 必然小于0 // 情况2: // now - previous > wait,即间隔的时间已经大于了传入定时器的时间 // remaining > wait 的情况如下: // 说明 now < previous 正常情况时绝对不会出现的,除非修改了电脑的本地时间,可以直接不考虑if (timeout) { // 定时器ID存在,就清除定时器 clearTimeout(timeout); timeout = null; // 清除定时器后,将timeout设置为null,这样就不会再次进入这个if语句 // 注意:比如 var xx = clearTimeout(aa),这里clearTimeout()不会把xx变成null,xx不会改变,但是aa不会执行} previous = now; // 马上缓存now,在执行func之前result = func.apply(context, args); // 执行funcif (!timeout) context = args = null; // 定时器ID不存在,就重置context和args // 注意:这里timeout不是一定为null的 // 1. 如果进入了上面的if语句,就会被重置为null // 2. 果如没有进入上面的if语句,则有可能是有值的} else if (!timeout && options.trailing !== false) { // 定时器ID不存在 并且 最后一次回调需要触发时进入 // later是回调 timeout = setTimeout(later, remaining); } return result; // 返回func的返回值 }; throttled.cancel = function() { // 取消函数 clearTimeout(timeout); // 清除定时器// 以下都是重置一切参数 previous = 0; timeout = context = args = null; }; return throttled; }; ---------------------------------------------------------- 总结整个流程: window.onscroll = _.throttle(fn, 1000); window.onscroll = _.throttle(fn, 1000, {leading: false}); window.onscroll = _.throttle(fn, 1000, {trailing: false}); 以点击触发_.throttle(fn, 1000)为例: 1. 第一次点击 (1)now赋值 (2)不会执行previous = now (3)remaining = wait - now => remain < 0 (4)进入if (remaining <= 0 || remaining > wait) 中 (5)previous = now; (6)执行 func.apply(context, args) (7)context = args = null 2. 第二次点击 - 迅速的 (1)now赋值 (2)进入if (!timeout && options.trailing !== false) 中(3)timeout = setTimeout(later, remaining); // 特别注意:这时timerout有值了!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // 而 timeout = null的赋值一共有两处!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! // (1)if (remaining <= 0 || remaining > wait) 这个if中修改!!!!!!!!!!!!!!!!! // (2)if (!timeout && options.trailing !== false)这个if的定时器回调中修改!!!!!!!!!! //而(2)中的定时器回调需要在remaining毫秒后才会修改!!!!!!!!!!!!!!!!!!!!!!!(4)previous = _.now(); 然后 timeout = null; 在然后 result = func.apply(context, args); (5)context = args = null; 3. 第三次点击 - 迅速的 - 因为在timeout存在,remaining毫秒还未到时,不会进入任何条件语句中执行任何代码 - 直到定时器时间到后,修改了timeout = null,previous被重新修改后就再做判断

Debounce: https://juejin.im/post/684490...
Throttle: https://juejin.im/post/684490...
分析underscore-throttle1:https://juejin.im/post/684490...
分析underscore-throttle2:https://github.com/lessfish/u...
underscore源码地址:https://github.com/jashkenas/...
https://juejin.im/post/684490...

    推荐阅读