关于javascript:深入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…

[[部署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 防抖函数

  • <font color=red>特点:延时执行,如果在延时的工夫内屡次触发,则从新计时</font>
  • 过程:当事件A产生时,设置一个定时器,a秒后触发A的回调函数,如果在a秒内有新的同一事件产生,则革除定时器,并从新开始计时(即又在a秒后触发A的回调,留神:上次的A的回调并未触发,而是定时器被革除了,定时器中A的回调就不会被执行)

版本一 (根底版本)

  • <font color=red>长处:能够传参,比方点击时,点击事件提供的 event 对象</font>
  • <font color=red>毛病:</font>

    • <font color=red>第一次触发是不须要延时的,版本一的第一次也是须要定时器的delay工夫后才会执行</font>
    • <font color=red>不能手动勾销debounce的执行,在delay工夫未到时的最初一次的执行</font>
    版本一 (根底版本)
    
    /**
     * @param {function} fn 须要debounce防抖函数解决的函数
     * @param {number} delay 定时器延时的工夫
     */
    function debounce(fn, delay) {
      let timer = null 
      // 该变量常驻内存,能够记住上一次的状态
      // 只有在外层函数失去援用时,该变量才会革除
      // 缓存定时器id
      
      return (...args) => {
        // 返回一个闭包
        // 留神参数:比方事件对象 event 可能获取到
        if (timer) {
          // timer存在,就革除定时器
          // 革除定时器,则定时器对应的回调函数也就不会执行
          clearTimeout(timer)
        }
        // 革除定时器后,从新计时
        timer = setTimeout(() => {
          fn.call(this, ...args)
          // this须要定时器回调函数时能力确定,this指向调用时所在的对象,大多数状况都指向window
        }, delay)
      }
    }

版本二 (降级版本)

  • <font color=red>解决问题:解决第一次点击不能立刻触发的问题</font>
  • <font color=red>解决问题:在delay工夫没有到时,手动的勾销debounce的执行</font>
  • 实现的后果:

    • 第一次点击立刻触发
    • 如果从第一次点击开始,始终不间断频繁点击(未超过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)
      }
    }
  }

<font color=red>版本三 (变更需要)</font>

  • <font color=red>需要:第一次立刻执行,而后等到进行触发delay毫秒后,才能够从新触发</font>

    版本三 (变更需要)
    需要:第一次立刻执行,而后等到进行触发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

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Document</title>
</head>
<body>
<div class="div">点击</div>
<script>
  const dom = document.getElementsByClassName('div')[0]
  const fn = () => {
    console.log(11111111111)
  }
 
  dom.addEventListener('click', debounce(fn, 1000, true), false)
  // document.addEventListener('click', (() => debounce(fn, 1000))(), false)
  // 留神:这里debounce(fn, 1000)会立刻执行,返回闭包函数
  // 留神:闭包函数才是在每次点击的时候触发
  
  function debounce(fn, delay, immediate) {
    let timer = null
    return (...args) => { // 这里能够拿到事件对象
      if (immediate && !timer) {
        // 如果立刻执行标记位是 true,并且timer不存在
        // 即第一次触发的状况
        // 当前的触发因为timer存在,则不再进入执行
        console.log('第一次立刻执行')
        fn.call(this, ...args)
      }
      if (timer) {
        clearTimeout(timer)
        // timer存在,就革除定时器
        // 革除定时器,则定时器对应的回调函数也就不会执行
      }
      timer = setTimeout(() => {
        console.log(args, 'args')
        console.log(this, 'this')
        fn.call(this, ...args)
      }, delay)
    }
  }
</script>
</body>
</html>

案例二 – 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 (
      <div className="App">
        <div onClick={debounce(fn, 3000, true)}>点击2</div>
        <div onClick={cancleDebounce}>勾销执行</div>
      </div>
    );
    }

    在实在我的项目中的使用

  • <font color=red>如视频监听断流的回调,会不停的执行监听函数,当视频当断流时,就不再执行监听函数了,此时能够用debounce,就能解决监听到断流后须要解决的事件,比方提醒断流</font>
  • input框的查问后果,不须要输出每个字符都去查问后果,而是应用debounce函数去解决查问后端接口
  • 小结:Debounce须要思考第一次执行,手动勾销执行,事件对象event等参数的传递问题

Throttle

  • <font color=red>特点:每隔一段时间,只执行一次</font>
  • 在工夫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)
      }
    }

    版本二(利用工夫戳)

  • <font color=red>原理:比拟两次点击的工夫戳差值(单位是毫秒),大于delay毫秒则执行fn</font>

    
    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
  • 滚动条的滚动监听函数须要触发的回调
  • 上拉加载更多

<font color=red>underscore中的Throttle</font>

前置常识:
- 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);
      // 执行func
      
      if (!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…

评论

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

这个站点使用 Akismet 来减少垃圾评论。了解你的评论数据如何被处理