• wordpress CMS主题:ssmay主题 wordpress CMS主题:ssmay主题
  • 首页 > 前端开发 > 每天阅读一个 npm 模块(4)- throttle-debounce

    每天阅读一个 npm 模块(4)- throttle-debounce

    作者: 分类:前端开发 点击: 2,006 次
    wordpress CMS主题:ssmay主题

      扫描下面的二维码,“关注”我的百家号。

      系列文章:

      1. 每天阅读一个 npm 模块(1)- username
      2. 每天阅读一个 npm 模块(2)- mem
      3. 每天阅读一个 npm 模块(3)- mimic-fn

      上一篇文章中介绍的属性描述符的知识太偏于理论,今天阅读的 throttle-debounce 模块会实用许多,在工作常常可以用到。

      一句话介绍

      今天阅读的 npm 模块是 throttle-debounce,它提供了 throttledebounce 两个函数:throttle 的含义是节流,debounce 的含义是防抖动,通过它们可以限制函数的执行频率,避免短时间内函数多次执行造成性能问题,当前包版本为 2.0.1,周下载量为 6.3 万。

      用法

      首选需要介绍一下 throttledebounce ,它们都可以用于 函数节流 从而提升性能,但它们还是存在一些不同:

      • debounce:将短时间内多次触发的事件合并成一次事件响应函数执行(往往是在第一次事件或者在最后一次事件触发时执行),即该段时间内仅一次真正执行事件响应函数。
      • throttle:假如在短时间内同一事件多次触发,那么每隔一段更小的时间间隔就会执行事件响应函数,即该段时间内可能多次执行事件响应函数。

      虽然每天最烦等电梯要花上十几分钟,但还是可以用坐电梯来举例子:

      • debounce:假如我在电梯里面正准备关门,这时 A 想要坐电梯,那么出于礼貌我会按下开门键,然后等他走进电梯后再尝试关门;等 A 进电梯后,又发现 B 也想要坐电梯,那么同样出于礼貌我会按下开门键,然后等他走进电梯。那么假如一直有人想要坐电梯的话,我就会不断地延后按下关门键的时机,直至没有人想要坐电梯(现实生活中我这样做的话,估计每天除了坐电梯就可以什么都不做了)。
      • throttle:实际上我每天都有工作要完成,不可能在电梯里无限地等别人。那么这回我任性一点,规定我只等 30 秒,不管到时候有没有人想要坐电梯,我都会按下关门键走掉。

      从上面两个例子中可以看出两者最大的区别在于只要有事件发生(有人想坐电梯),若使用了 throttle 方法,那么在一段时间内事件响应函数一定会执行(30秒内我按下关门键);若使用了 debounce 方法,那么只有事件停止发生后(我发现没有人想坐电梯)才会执行。

      大家可以尝试在下面的 Demo 中滚动鼠标直观地感受到这两者的不同:

      See the Pen The Difference Between Throttling, Debouncing, and Neither by Elvin Peng (@elvinn) on CodePen.

      对于 throttle-debounce,它的简单用法如下:

      import { throttle, debounce } from 'throttle-debounce';
      
      function foo() { console.log('foo..'); }
      function bar() { console.log('bar..'); }
      
      const fooWrapper = throttle(200, foo);
      
      for (let i = 1; i < 10; i++) {
        setTimeout(fooWrapper, i * 30);
      }
      
      // => foo 执行了三次
      // => foo..
      // => foo..
      // => foo..
      
      const barWrapper = debounce(200, bar);
      
      for (let i = 1; i < 10; i++) {
        setTimeout(barWrapper, i * 30);
      }
      
      // => bar 执行了一次 
      // => bar..
      
      

      源码学习

      throttle 实现

      将源码简化后适当修改如下:

      // 源码 4-1
      function throttle(delay, callback) {
        let timeoutID;
        let lastExec = 0;
      
        function wrapper() {
          const self = this;
          const elapsed = Number(new Date()) - lastExec;
          const args = arguments;
      
          function exec() {
            lastExec = Number(new Date());
            callback.apply(self, args);
          }
      
          clearTimeout(timeoutID);
      
          if (elapsed > delay) {
            exec();
          } else {
            timeoutID = setTimeout(exec, delay - elapsed);
          }
        }
      
        return wrapper;
      }
      

      整个代码的逻辑十分清晰,一共只有三步:

      1. 计算距离最近一次函数执行后经过的时间 elapsed,并清除之前设置的计时器。
      2. 如果经过的时间大于设置的时间间隔 delay,那么立即执行函数,并更新最近一次函数的执行时间。
      3. 如果经过的时间小于设置的时间间隔 delay,那么通过 setTimeout 设置一个计数器,让函数在 delay - elapsed 时间后执行。

      源码 4-1 并不难理解,不过需要关注一下 this 的使用:

      function throttle(delay, callback) {
          // ...
          function wrapper() {
          	const self = this;
              const args = arguments;
              // ...
              
              function exec() {
                  // ...
      	      	callback.apply(self, args);
          	}
              
          }
      }
      

      在上面的代码中,通过 self 变量临时保存 this 的值,从而在 exec 函数中通过 callback.apply(self, args) 传入正确的 this 值,这种做法在闭包相关的函数调用中十分常用。正因为这里对 this 的处理,所以可以实现下面的能力:

      function foo() { console.log(this.name);  }
      
      const fooWithName = throttle(200, foo);
      
      const obj = {name: 'elvin'};
      
      fooWithName.call(obj, 'elvin');
      
      // => 'elvin'
      

      debounce 实现

      由于 debouncen 只是往后推延函数的执行时间,并不具有 throttle 每隔一段时间一定会执行的能力,所以其实现起来更加简单:

      function debounce(delay, callback) {
        let timeoutID;
      
        function wrapper() {
          const self = this;
          const args = arguments;
      
          function exec() {
            callback.apply(self, args);
          }
      
          clearTimeout(timeoutID);
      
          timeoutID = setTimeout(exec, delay);
        }
      
        return wrapper;
      }
      

      将上述代码与 throttle 实现的代码相比,可以发现其就是去除了 elapsed 相关逻辑后的代码,其余大部分代码一模一样,所以 debounce 函数可以借助 throttle 函数实现(throttle-debounce 源代码中也是这样做的),throttle 函数也可以借助 debounce 函数实现。

      throttle 和 debounce 使用场景举例

      throttledebounce 适用于用户短时间内频繁执行某一相同操作的场景,例如:

      • 用户拖动浏览器窗口改变窗口大小,触发 resize 事件。
      • 用户移动鼠标,触发 mousemove 等事件。
      • 用户在输入框内进入输入,触发 keydown | keypress | keyinput | keyup 等事件。
      • 用户滚动屏幕,触发 scroll 事件。
      • 用户在点击按钮后,由于 API 请求耗时未立即看到响应,可能会不断点击按钮触发 click 事件。

      在网上搜索了不少资料,发现对两个函数的使用场景有时彼此之间都互相矛盾,例如有的说在搜索框进行输入,应该使用 debounce 进行限流,从而减轻服务器压力;有的说使用 throttle 进行限流即可,可以更快地返回用户的搜索结果。

      在我看来,并不存在一个场景,就一定是使用 throttledebounce 中的一种方法并另外一种方法好,往往需要结合自身的情况进行考虑和选择:

      • 当事件响应函数对 CPU、GPU、流量、服务器等资源的占用在接受范围内时,可以使用 throttle 进行限流带来更好的用户体验。
      • 当事件响应函数对 CPU、GPU、流量、服务器等资源的占用较大时,可以使用 debounce 进行更强力的限流,从而减轻压力。

      写在最后

      throttle-debounce 源码和我前几天所看的 Sindre 所写的模块代码风格完全不同,它的代码中注释的行数约为代码行数的三倍,而且函数的参数均有详细的注释,这本应是一件好事,但是对于我阅读源码而言,并没有觉得更加轻松,而求由于对可选参数进行的如下处理,让我阅读起来更加费力:

      // 源码 4-2
      
      /**
       *
       * @param  {Number}    delay
       * @param  {Boolean}   [noTrailing]
       * @param  {Function}  callback
       * @param  {Boolean}   [debounceMode]
       *
       * @return {Function}  A new, throttled, function.
       */
      export default function ( delay, noTrailing, callback, debounceMode ) {
          // `noTrailing` defaults to falsy.
      	if ( typeof noTrailing !== 'boolean' ) {
      		debounceMode = callback;
      		callback = noTrailing;
      		noTrailing = undefined;
      	}
          
          // ...
      }
      

      在源码 4-2 中,从注释可以看出 noTrailingdebounceMode 是可选参数,delay 和 callback 为必选参数,然后它将可选参数 noTrailing 放在了必选参数 callback 之前,再在函数中的代码进行判断:假如 noTrailing 为函数的话,则此值应作为 callback,然后再将 noTrailing 设为默认值 undefined

      不禁感叹这真是一番骚操作,哪怕是为了兼容 ES5,也有更好的写法,这里说说我个人认为可用 ES6 语法时更好的写法:

      export default function (dalay, noTrailing, options = {
          callback = false,
          debounceMode = false,
      } = {}) {
          // ...
      }
      

      关于我:毕业于华科,工作在腾讯,elvin 的博客 欢迎来访 ^_^

      本文转载于:猿2048https://www.mk2048.com/blog/blog_hbkk2a0a2j.html



      欢迎“关注”我的百家号。

      头条二维码
      加入我的QQ群
      头条二维码
      关注我的百家号

    文章作者:sunny
    本文地址:http://wanlimm.com/77202006218383.html
    版权所有 © 转载时必须以链接形式注明作者和原始出处!

    上一篇:
    下一篇:
    wordpress CMS主题:ssmay主题

    或许你会感兴趣的文章:

    发表评论

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

    此站点使用Akismet来减少垃圾评论。了解我们如何处理您的评论数据