js 消抖(debounce)与节流(throttle)

2,028 阅读3分钟

博客园地址

前言

故事发生在与大创的又一次撕逼(日常)中,我方坚定的认为:作为社会主义接班人,节流与消抖的界限是明显的,是不容混肴的,是不可侵犯的!对方辩友坚持地觉得:界限是模糊的,行为是暧昧的,性别是可以忽视的(。。。)。本着凡事要往祖坟上刨的精神,对这两个概念进行了一番深刻的社会主义改造。

定义

节流(throttle)

定义: 如果一个函数持续的,频繁地触发,那么让它在一定的时间间隔后再触发。

感觉像是去排队过安检,当人很多的时候(持续地要进门),安保会隔一段时间放进去几个进行安检(一定时间的间隔)。

类似这种的feel

消抖(debounce)

定义: 如果一个函数持续地触发,那么只在它结束后过一段时间只执行一次。

像是两个人的对话,A在不停的balabala(持续触发),如果他说话的时候有停顿(一定间隔),但是停顿的时间不够长,就认为A没有说完, 当停顿时间超过一某个范围就认为A说完了,然后B开始回答(响应)。

场景

前提其实都是某个行为持续地触发,不同之处只要判断是要优化到减少它的执行次数还是只执行一次就行。

举个例子,像dom的拖拽,如果用消抖的话,就会出现卡顿的感觉,因为只在停止的时候执行了一次,这个时候就应该用节流,在一定时间内多次执行,会流畅很多。

但是,如果是输入联想这种,我要输入“谁是世界上最帅的人?”,多次执行的话,可能当我输到最的时候,就去搜索,结果搜出来一堆“谁是世界上最笨”,“谁是世界上最胖”之类的不必要搜索,只需要在输入完成后进行搜索,消抖当时最合适啦。

实现

明白了定义与场景,让我们脱下裤子, 呸 撸起袖子来造一个。

先看看名家手笔

underscore的实现

复制代码
  // Returns a function, that, as long as it continues to be invoked, will not
  // be triggered. The function will be called after it stops being called for
  // N milliseconds. If `immediate` is passed, trigger the function on the
  // leading edge, instead of the trailing.
  _.debounce = function(func, wait, immediate) {
    var timeout, result;

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

    var debounced = restArgs(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;
  };
复制代码
  // Returns a function, that, when invoked, will only be triggered at most once
  // during a given window of time. Normally, the throttled function will run
  // as much as it can, without ever going more than once per `wait` duration;
  // but if you'd like to disable the execution on the leading edge, pass
  // `{leading: false}`. To disable execution on the trailing edge, ditto.
  _.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;
  };

实现了更灵活的option去控制是否立即执行,是否劫数是在执行以及增加cancel的控制逻辑。本着大道至简的精神(其实是懒),我们来改一个精简版只实现基本业务。

复制代码
function debounce (fn, delay) {
    let args    = arguments,
       context = this,
        timer   = null;

    return function () {
        if (timer) {
            clearTimeout(timer);

            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        } else {
            timer = setTimeout(function () {
                fn.apply(context, args);
            }, delay);
        }
    }
}

复制代码
function throttle (fn, delay) {
    let timer    = null,
        remaining   = 0,
        previous = new Date();

    return function () {
        let now       = new Date(),
            args      = arguments,
            context   = this;
            remaining = now - previous;

        if (remaining >= delay) {
            if (timer) {
                clearTimeout(timer);
            }

            fn.apply(context, args);
            previous = now;
        } else {
            if (!timer) {
                timer = setTimeout(function () {
                    fn.apply(context, args);
                    previous = new Date();
                }, delay - remaining);
            }
        }
    };
}
复制代码

吐槽

红宝书上给的节流例子,嗯。。。怎么说呢。。。 是非常棒的消抖的实现,(╯‵□′)╯︵┻━┻谢谢!!!!!