js 的函数节流(throttle)和函数防抖(debounce)概述

函数防抖(debounce)

一个事件频繁触发,但是我们不想让他触发的这么频繁,于是我们就设置一个定时器让这个事件在 xxx 秒之后再执行。如果 xxx 秒内触发了,则清理定时器,重置等待事件 xxx 秒
比如在拖动 window 窗口进行 background 变色的操作的时候,如果不加限制的话,随便拖个来回会引起无限制的页面回流与重绘
或者在用户进行 input 输入的时候,对内容的验证放在用户停止输入的 300ms 后执行(当然这样不一定好,比如银行卡长度验证不能再输入过程中及时反馈)

一段代码实现窗口拖动变色

  <script>
    let body = document.getElementsByTagName("body")[0];
    let index = 0;
    if (body) {
      window.onresize = function() {
        index++;
        console.log("变色" + index + "次");
        let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)];
        body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")"; //晃瞎眼睛
      };
    }
  </script>

使用 setTimeout 进行延迟处理,每次触发事件时都清除掉之前的方法

  <script>
    let body = document.getElementsByTagName("body")[0];
    let index = 0;
    let timer = null;
    if (body) {
      window.onresize = function() {
        //如果在一秒的延迟过程中再次触发,就将定时器清除,清除完再重新设置一个新的
        clearTimeout(timer);

        timer = setTimeout(function() {
          index++;
          console.log("变色" + index + "次");
          let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)];
          body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")";
        }, 1000); //反正就是等你什么都不干一秒后才会执行代码
      };
    }
  </script>

但是目前有一个问题,就是代码耦合,这样不够优雅(b 格),将防抖和变色分离一下

  <script>
    let body = document.getElementsByTagName("body")[0];
    let index = 0;
    let lazyLayout = debounce(changeBgColor, 1000);
    window.onresize = lazyLayout;

    function changeBgColor() {
      index++;
      console.log("变色" + index + "次");
      let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)];
      body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")";
    }

    //函数去抖(连续事件触发结束后只触发一次)
    function debounce(func, wait) {
      let timeout, context, args; //默认都是undefined

      return function() {
        context = this;
        args = arguments;

        if (timeout) clearTimeout(timeout);

        timeout = setTimeout(function() {
          //执行的时候到了
          func.apply(context, args);
        }, wait);
      };
    }
  </script>

当然 debounce 在一些有成熟的实现,underscore.js 的 debounce

//1.9.1
 _.debounce = function(func, wait, immediate) {
    var timeout, result;

    var later = function(context, args) {
      timeout = null;
      if (args) result = func.apply(context, args);
    };

    var debounced = restArguments(function(args) {
      if (timeout) clearTimeout(timeout);
      if (immediate) {
        var callNow = !timeout;
        timeout = setTimeout(later, wait);
        if (callNow) result = func.apply(this, args);
      } else {
        timeout = _.delay(later, wait, this, args);
      }

      return result;
    });

    debounced.cancel = function() {
      clearTimeout(timeout);
      timeout = null;
    };

    return debounced;
  };

函数节流(throttle)

一个事件频繁触发,但是在 xxx 秒内只能执行一次代码

//上面的变色在节流中就是这样写了
  <script>
    let doSomething = true;
    let body = document.getElementsByTagName("body")[0];
    let index = 0;

    window.onresize = function() {
      if (!doSomething) return;
      doSomething = false;
      setTimeout(function() {
        index++;
        console.log("变色" + index + "次");
        let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)];
        body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")";
        doSomething = true;
      }, 1000);
    };
  </script>

跟上面的防抖差不多,随便分离一下,降低代码的耦合度

<script>
    let body = document.getElementsByTagName("body")[0];
    let index = 0;
    let lazyLayout = throttle(changeBgColor, 1000);
    window.onresize = lazyLayout;

    function changeBgColor() {
      index++;
      console.log("变色" + index + "次");
      let num = [Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255), Math.ceil(Math.random() * 255)];
      body.style.background = "rgb(" + num[0] + "," + num[1] + "," + num[2] + ")";
    }

    //函数去抖(连续事件触发结束后只触发一次)
    function throttle(func, wait) {
      let context,
        args,
        doSomething = true;

      return function() {
        context = this;
        args = arguments;

        if (!doSomething) return;

        doSomething = false;

        setTimeout(function() {
          //执行的时候到了
          func.apply(context, args);
          doSomething = true;
        }, wait);
      };
    }
  </script>

underscore 中关于 throttle 的实现

  _.throttle = function(func, wait, options) {
    var timeout, context, args, result;
    var previous = 0;
    if (!options) options = {};

    var later = function() {
      previous = options.leading === false ? 0 : _.now();
      timeout = null;
      result = func.apply(context, args);
      if (!timeout) context = args = null;
    };

    var throttled = function() {
      var now = _.now();
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      if (remaining <= 0 || remaining > wait) {
        if (timeout) {
          clearTimeout(timeout);
          timeout = null;
        }
        previous = now;
        result = func.apply(context, args);
        if (!timeout) context = args = null;
      } else if (!timeout && options.trailing !== false) {
        timeout = setTimeout(later, remaining);
      }
      return result;
    };

    throttled.cancel = function() {
      clearTimeout(timeout);
      previous = 0;
      timeout = context = args = null;
    };

    return throttled;
  };

防抖和节流是用来干啥的?

防抖

  • 绑定 scroll 滚动事件,resize 监听事件
  • 鼠标点击,执行一个异步事件,相当于让用户连续点击事件只生效一次(很有用吧)
  • 还有就是输入框校验事件(但是不一定好使,比如校验银行卡长度,当你输入完之后已经超出 100 个字符,正常应该是超出就提示错误信息)

节流

  • 当然还是鼠标点击啦,但是这个是限制用户点击频率。类似于你拿把 ak47 射击,枪的射速是 100 发/分钟,但是的手速达到 1000 按/分钟,就要限制一下喽(防止恶意刷子)
  • 加载更多的功能

文章写的时候用的 underscore 1.8.2 版本,实现也是参考 underscore 的源码,实现方式与 underscore 最新有些代码还是不太一样了。(功能还是相同的)

02-23 02:02