最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • javascript函数的防抖(debounce)与节流(throttle)详解

    正文概述 掘金(爱猫的小鱼儿)   2021-02-22   535

    一、序言:

    我们在平时开发的时候,会有很多场景会频繁触发事件,比如说搜索框实时发请求,onmousemove, resize, onscroll等等,有些时候,我们并不能或者不想频繁触发事件,怎么办呢?这时候就应该用到函数防抖和函数节流了!


    先看一个例子

    <div id="content" style="height:150px;line-height:150px;text-align:center; color: #fff;background-color:#ccc;font-size:80px;"></div>
     
    <script>
      let num = 1;
      let content = document.getElementById('content');
    
      var lastDate = new Date().getTime();
    
      function count() {
          content.innerHTML = num++;
    
          var nowDate = new Date().getTime();
          // 输出两次相隔执行时间差
          console.log(nowDate-lastDate)
          lastDate = nowDate
      };
      content.onmousemove = count;
    </script>
    

    这段代码, 在灰色区域内鼠标随便移动,就会持续触发 count() 函数,导致的效果如下:

    javascript函数的防抖(debounce)与节流(throttle)详解

    控制台输出的count()函数执行间隔时间如下:

    javascript函数的防抖(debounce)与节流(throttle)详解


    接下来我们通过防抖和节流限制频繁操作。

    二、函数防抖(debounce)

    【概念】在事件被触发n秒后再执行回调,如果在这n秒内又被触发,则重新计时。

    【生活中的实例】如果有人进电梯(触发事件),那电梯将在10秒钟后出发(执行事件监听器),这时如果又有人进电梯了(在10秒内再次触发该事件),我们又得等10秒再出发(重新计时)。

    有两个版本:非立即执行版和立即执行版

    2.1、非立即执行版(延迟执行)

    // 非立即执行版(延迟执行)
    function debounce(func, wait) {
      var timer;
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
    
        if (timer) clearTimeout(timer);
    
        timer = setTimeout(function() {
          func.apply(this, args)
        }, wait)
      }
    }
    
    // 使用方式如下:
    content.onmousemove = debounce(count,1000);
    

    非立即执行版的意思是:触发事件后函数不会立即执行,而是在 n 秒后执行,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。只到下个 n 秒内没有再触发事件,才会被执行。效果如下:

    javascript函数的防抖(debounce)与节流(throttle)详解

    经过“函数防抖”处理后,控制台输出的count()函数执行间隔时间如下:

    javascript函数的防抖(debounce)与节流(throttle)详解


    使用事件监听的方式(有时需要对事件绑定和解绑,如在vue和react中)

    // 重新声名一个防抖后的函数
    var handleMousemove = debounce(count,1000)
    
    // 绑定
    window.addEventListener('mousemove', handleMousemove)
    
    // 解绑
    window.removeEventListener('mousemove', handleMousemove)
    

    2.2、立即执行版

    // 立即执行版
    function debounce(func, wait) {
      var timer;
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
    
        if (timer) clearTimeout(timer);
    
        var callNow = !timer;
    
        timer = setTimeout(function() {
          // 设置为null即下次callNow为true,即可实现下次执行。
          timer = null;
        }, wait)
    
        if (callNow) func.apply(context, args);
      }
    }
    
    // 使用方式如下:
    content.onmousemove = debounce(count,1000);
    

    立即执行版的意思是触发事件后函数会立即执行,然后 n 秒内不触发事件才能继续执行函数的效果。用法同上,效果如下:

    javascript函数的防抖(debounce)与节流(throttle)详解


    2.3、合成版

    // 合成版
    /**
     * @desc 函数防抖
     * @param func 目标函数
     * @param wait 延迟执行毫秒数
     * @param immediate true - 立即执行, false - 延迟执行
     */
    
    function debounce(func, wait, immediate) {
      var timer;
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
              
        if (timer) clearTimeout(timer);
        if (immediate) {
          var callNow = !timer;
          timer = setTimeout(function() {
            // 设置为null即下次callNow为true,即可实现下次执行。
            timer = null;
          }, wait);
          if (callNow) func.apply(context, args);
        } else {
          timer  = setTimeout(function() {
            func.apply(context, args);
          }, wait)
        }
      }
    }
    

    三、函数节流(throttle)

    【概念】规定一个单位时间,在这个单位时间内,只能有一次触发事件的回调函数执行,如果在同一个单位时间内某事件被触发多次,只有一次能生效。

    【生活中的实例】一种说法是当 1 秒内连续播放 24 张以上的图片时,在人眼的视觉中就会形成一个连贯的动画,所以在电影的播放(以前是,现在不知道)中基本是以每秒 24 张的速度播放的,为什么不 100 张或更多是因为 24 张就可以满足人类视觉需求的时候,100 张就会显得很浪费资源。

    同样有两个版本,立即执行版(时间戳版)和延迟执行版(定时器版)。

    3.1、立即执行版(时间戳版)

    // 立即执行版(时间戳版)
    function throttle(func, wait) {
      var previous = 0;
      return function() {
        var now = Date.now();
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
        if (now - previous > wait) {
          func.apply(context, args);
          previous = now;
        }
      }
    }
    
    // 使用方式如下:
    content.onmousemove = throttle(count,1000);
    

    效果如下:

    javascript函数的防抖(debounce)与节流(throttle)详解

    可以看到,在持续触发事件的过程中,函数会立即执行,并且每 1s 执行一次。

    经过“函数防抖”处理后,控制台输出的count()函数执行间隔时间如下:

    javascript函数的防抖(debounce)与节流(throttle)详解

    可以看出执行频率约等于1000ms,但又都大于1000ms。从而实现指定频率并节流的效果。


    3.2、延迟执行版(定时器版)

    // 延迟执行版(定时器版)
    function throttle(func, wait) {
      var timer;
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
        if (!timer) {
          timer = setTimeout(() => {
            timer = null;
            func.apply(context, args)
          }, wait)
        }
      }
    }
    
    // 使用方式如下:
    content.onmousemove = throttle(count,1000);
    

    用法同上,效果如下:

    javascript函数的防抖(debounce)与节流(throttle)详解

    可以看到,在持续触发事件的过程中,函数不会立即执行,并且每 1s 执行一次,在停止触发事件后,函数还会再执行一次。


    同样地,我们也可以将时间戳版和定时器版的节流函数结合起来,实现合成版的节流函数。

    3.3、合成版

    // 合成版
    /**
     * @desc 函数节流
     * @param func 函数
     * @param wait 延迟执行毫秒数
     * @param immediate true - 立即执行(时间戳版), false - 延迟执行(定时器版)
     */
    function throttle(func, wait, immediate) {
      if (immediate) {
        var previous = 0;
      } else {
        var timer;
      }
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
        if (immediate) {
          var now = Date.now();
    
          if (now - previous > wait) {
            func.apply(context, args);
            previous = now;
          }
        } else {
          if (!timer) {
            timer = setTimeout(() => {
              timer = null;
              func.apply(context, args)
            }, wait)
          }
        }
      }
    }
    

    四、函数防抖和函数节流各自应用的场景

    4.1、对于函数防抖,有以下几种应用场景:

    1. 对于输入框连续输入进行AJAX验证时,用函数防抖能有效减少请求次数。
    2. 判断scroll是否滑到底部。
    3. 给按钮加函数防抖防止表单多次提交。

    总的来说,适合多次事件一次响应的情况

    4.2、对于函数节流,有如下几个场景:

    1. DOM元素拖拽
    2. Canvas画笔功能
    3. 游戏中的刷新率
    4. onmousemove, resize, onscroll等事件

    总的来说,适合大量事件按时间做平均分配触发。


    五、附录:

    5.1、关于节流/防抖函数中“this的指向”和“arguments”解析:

    我们来分析一下以下代码

    function throttle(func, wait) {
      var timer;
      return function() {
        var context = this; // 注意 this 指向
        var args = arguments; // arguments中存着event
        if (!timer) {
          timer = setTimeout(() => {
            timer = null;
            func.apply(context, args)
          }, wait)
        }
      }
    }
    
    // 使用方式如下:
    content.onmousemove = throttle(count,1000);
    

      首先,在执行throttle(count, 1000)这行代码的时候,会有一个返回值,这个返回值是一个新的匿名函数,因此content.onmousemove = throttle(count,1000)这句话最终可以这样理解:

    content.onmousemove = function() {
      var context = this; // 注意 this 指向
      var args = arguments; // arguments中存着event
      
      // var args = Array.prototype.slice.call(arguments, 1); // 这样可以去掉arguments中的event
      
      console.log('this', this); // 输出contentDOM元素
      console.log('arguments', arguments); // 输出带有event的数组
      
      if (!timer) {
        timer = setTimeout(() => {
          timer = null;
          func.apply(context, args)
        }, wait)
      }
    }
    

      到这边为止,只是绑定了事件函数,还没有真正执行,而this的具体指向需要到真正运行时才能够确定下来。

      其次,当我们触发onmousemove事件的时候,才真正执行了上述的匿名函数,即content.onmousemove()。此时,上述的匿名函数的执行是通过对象.函数名()来完成的,那么函数内部的this自然指向对象(content)

      最后,匿名函数内部的func的调用方式如果是最普通的直接执行func(),那么func内部的this必然指向window,这将会是一个隐藏bug!所以,我们通过匿名函数捕获this,然后通过func.apply()的方式修改this指向。


    5.2、函数节流的最佳时间间隔

    下面我们再看一个页面滚动事件的例子。

    <script>
    var num = 1;
    var content = document.getElementById('content');
    var lastDate = new Date().getTime();
    
    function count() {
      content.innerHTML = num++;
    
      var nowDate = new Date().getTime();
      // 输出两次相隔执行时间差
      console.log(nowDate-lastDate)
      lastDate = nowDate
    };
    
    
    window.onscroll = throttle(count, 10);
    

    我们把节流时间间隔设置为10ms,按道理输出的时间间隔最小的应该有10的。但是实现输出基本上都是不小于16。

    javascript函数的防抖(debounce)与节流(throttle)详解

    而鼠标移动事件的触发时间间隔却很小。(下图为上面例子中的鼠标移动事件未做防抖和节流的输出结果)

    javascript函数的防抖(debounce)与节流(throttle)详解

    为什么会这样子呢?

    在说原因之前,我们先说一下另一个东西——显示器刷新频率。一般显示器的刷新频率是60Hz,我的显示器的刷新频率也是60Hz。这个60Hz的意思是1秒内显示器画面刷新的频率。也就可以计算出60Hz的显示器每次刷新的间隔是1000/60(约等于16)。

    如果我们把显示器的刷新频率改掉,如改成48Hz。每次刷新间隔则是1000/48(约等于20)。

    javascript函数的防抖(debounce)与节流(throttle)详解

    输出结果如下图:

    javascript函数的防抖(debounce)与节流(throttle)详解

    所以滚动事件时,函数执行的间隔最小值取绝于显示器的刷新频率。大部分显示器的刷新频率都是60Hz,所以一般最小时间间隔为16ms。有一些玩游戏的朋友显示器可能会要求高一些,刷新频率也就高一些。

    所以如果是鼠标移动事件,触发时间间隔即使小于16ms,但是用户看到的最快也只会是16ms。这样子就会造成部分计算的浪费。所以函数节流的最值时间间隔应该设置为16ms。如果是执行的计算量过大,明显影响性能,也可以适当调大。具体以不影响用户体验为标准,建议不要大于30ms。


    扩展阅读

    1. js 函数的防抖(debounce)与节流(throttle)
    2. 详解JS函数柯里化
    3. JS函数防抖和函数节流

    文章来源

    javascript函数的防抖(debounce)与节流(throttle)详解-杨会清的个人网站


    起源地下载网 » javascript函数的防抖(debounce)与节流(throttle)详解

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元