wulang8353 / DO-THE-JS-BETTER

:trollface: 我的真阳为至宝,岂肯轻与你这粉骷髅——JS :tada:

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

我觉得别人写的蛮好的,防抖与节流傻傻分不清楚

wulang8353 opened this issue · comments

我觉得别人写的蛮好的,防抖与节流傻傻分不清楚

每日一话:这位朋友,能否借一生说话

函数节流和函数防抖,两者都是优化高频率执行 js 代码的一种手段。

防抖

函数防抖是指触发事件后,在 n 秒内函数只能执行一次,如果在 n 秒内又触发了事件,则会重新计算函数执行时间。通俗一点来说就是,当一个事件连续触发后,只执行最后一次。

函数防抖的应用场景一般是连续的事件,只需触发一次回调:

  • 窗口大小 Resize。无论用户手抖调整了 N 次,但都以最后一次为准计算窗口大小进行渲染

  • 搜索框输入提示。当用户最后一次输入完以后,再发送请求

  • 手机号、邮箱验证输入检测。

简单实现

function debounce1(func, wait){
  var timeout;

  return funtion(){
    var context = this;  // 修复this的指向问题,否则将执行window
    var args = argments; // func函数可以传参

    clearTimeout(timeout)
    timeout = setTimeout(function(){
      func.apply(context, args)
    }, wait)
  }
}

函数 debounce1 中,通过闭包返回添加防抖功能的函数 func。 防抖过程是通过首先清除定时器 timeout,随后再设置定时器。这样就实现了无论多少次触发事件,只有最后一次触发才会被执行。

这里需要注意的是由于闭包导致的上下文环境的指向问题。其实,基本上涉及闭包的地方,除去用 ES6 的箭头函数自动绑定函数定义时的 this,都需要手动指定 this。

需求添加:立即执行

上面代码有个问题在于,非要等到最后一次事件触发的 N 秒后才会执行,但我希望第一次触发的时候就立即执行。

例如手机号短信验证,我第一次点击的时候就希望立刻发短信,而不是按照上面代码那样,必须等到一定延时后才执行操作。

所以咱们加个标志位进行验证判断是否立即执行。

function debounce2(func, wait, immediate) {
  var timeout;

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

    if (timeout) clearTimeout(timeout);
    if (immediate) {
      var flag = !timeout;
      timeout = setTimeout(function() {
        timeout = null;
      }, wait);
      if (flag) func.apply(context, args);
    } else {
      timeout = setTimeout(function() {
        func.apply(context, args);
      }, wait);
    }
  };
}

代码的核心**在于首次执行时定时器不存在,立即执行函数,异步任务设置定时器。若定时器存在,则不能立即执行函数。

第一次触发事件,timeout 为 undefined,所以 flag 为 true。设置定时器,由于标志位为 true,立即执行函数。

若在 wait 时间内重复触发,由于定时器 timeout 已经存在,所以 flag 一直为 false,事件不再执行。由于定时器回调函数中令 timeout=null,所以若在 wait 时间后触发函数,flag 为 true,相当于第一次触发事件那样立即执行函数了。

需求添加: 取消防抖

比如发送验证码功能,点击按钮后会发送短信都手机,但是要等待 60s 才能重复发送。这时候可以设置一个取消按钮,就不用等待而立即发送短信了。

function debounce3(func, wait, immediate) {
  var timeout, result;

  var debounced = function() {
    var context = this;
    var args = arguments;

    if (timeout) clearTimeout(timeout);
    if (immediate) {
      var flag = !timeout;
      timeout = setTimeout(function() {
        timeout = null;
      }, wait);
      if (flag) func.apply(context, args);
    } else {
      timeout = setTimeout(function() {
        func.apply(context, args);
      }, wait);
    }
    return result;
  };

  debounced.cancel = function() {
    clearTimeout(timeout); // 清除定时器, 立即执行
    timeout = null;
  };

  return debounced;
}

// 使用方法
var action = debounce(fn, 10000, true);

element.onmousemove = action;
document.getElementById("button").addEventListener("click", function() {
  action.cancel();
});

节流

函数节流是持续触发事件,每隔一段时间,只执行一次事件。比如人的眨眼睛,就是一定时间内眨一次。不同于防抖的地方在于,每过 1s 触发一次事件,若在 4.2s 停止触发,节流就不会再执行函数。而函数防抖则以最后一次事件触发为准,会在一定延迟后执行函数。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。
我们用 starting 代表首次是否执行,ending 代表结束后是否再执行一次。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器

时间戳

时间戳的差值与 wait 比较

当事件触发时,比较当前时间与上一次时间(初始值为 0)的差值,若该值大于 wait,则执行函数,并更新上一次时间。若小于则不执行。

function throttle1(func, wait) {
  var context;
  var previous = 0;

  return function() {
    var now = +new Date();
    context = this;
    if (now - previous > wait) {
      func.apply(context, arguments);
      previous = now;
    }
  };
}

定时器

定时器一定要执行完才可以执行函数

当事件触发时,设置定时器。当事件重复触发,若定时器存在,则不再执行函数。知道定时器执行完后,再执行函数,并清空定时器。

function throttle2(func, wait) {
  var timeout, context;

  return function() {
    context = this;
    if (!timeout) {
      timeout = setTimeout(function() {
        timeout = null;
        func.apply(context, arguments);
      }, wait);
    }
  };
}

所以比较两个方法:

第一种事件初次会立刻执行(starting),第二种事件会在 n 秒后第一次执行
第一种事件停止触发后没有办法再执行事件,第二种事件停止触发后依然会再执行一次事件(ending)

既...又...

既能立刻执行,又能在停止触发的时候还能再执行一次,真的是很贪心呢。

function throttle(func, wait) {
  var timeout, context, args, result;
  var previous = 0;

  var later = function() {
      previous = +new Date();
      timeout = null;
      func.apply(context, args)
  };

  var throttled = function() {
      var now = +new Date();
      var context = this;
      var args = arguments;

      //下次触发 func 剩余的时间
      var remaining = wait - (now - previous);
      
      // 如果没有剩余的时间了或者你改了系统时间
      // (now - previous) >= wait || previous > now
      // starting 初次直接执行
      // 若触发事件小于
      if (remaining <= 0 || remaining > wait) {
          if (timeout) {
              clearTimeout(timeout);
              timeout = null;
          }
          previous = now;
          func.apply(context, args);
      } else if (!timeout) {
          // ending
          timeout = setTimeout(later, remaining);
      }
  };
  return throttled;
}

若重复触发事件小于wait,当第一次执行完后,走else if语句,设置定时器。此时的延时事件为触发func的剩余时间remaining。
过remaining后,执行函数later。其作用是更新上一次时间戳,执行函数。

参考

JavaScript专题之跟着underscore学防抖
JavaScript专题之跟着underscore学节流