防抖/节流到底解决什么问题?

1,424 阅读4分钟

有这么一句话“一个好的问题,比正确的答案重要”,也许这样能更好的帮助我们改变对原有的一些看法。

防抖/节流到底解决什么问题呢?

对于用户而言,过程只是获取结果的一个中间产物,比如用户在input 框中输入文字hello world,只有完整的输入字单词,并搜索出结果才是用户期待的。像向下滚动拉去更多的内容,鼠标移动画出运动轨迹...这一类高频触发事件的场景,如果每一次的触发都要一一响应,就会性能消耗比较大,浪费资源,对用户来讲又没有任何意义。

防抖/节流只是为了解决以上的问题,提出的2个解决方案。在此应当明确问题的关键点:

  1. 高频触发:用户的行为高频的发生,注意不限前台哦,后台也是可以的
  2. 忽略过程:触发过程对用户来讲,可以忽略不用处理,只响应最近的一次用户操作就可以了

防抖/节流:是时候展现真正的技术了

防抖的算法实现

  • 防抖:从最近一次触发开始计时,设定一个延后执行时间,此期间如果再触发,重新时计,执行的时间是向后移的
  • 算法:开始计时,如果在此期间触发,则重新计算时间(减去最近一次的触发时间,而不是不断的置空/新加定时器)。到执行时已经超出设定的时间间隔,则马上执行。
// loadsh.debounce 的算法实现,为了更好的理解,对原代码做了一些简化
function debounce(func, wait, immediate){
  var timeout, args, context, timestamp, result;
  if (null == wait) wait = 100;

  function later() {
    // last 等于当前时间差动最近一次时间戳
    // 也就是 Date.now() - 10ms
    var last = Date.now() - timestamp 约等于90ms;
    if (last < wait && last >= 0) {
      // 重新设定延时执行时间 wait - last
      // 假设wait:100ms,那么100 - 90 = 10ms
      // 那么再延后10ms 就是从最近一次触发,到fun 的执行,刚好是 wait这个时间间隔
      timeout = setTimeout(later, wait - last);
    } else {
      timeout = null;
      if (!immediate) {
        result = func.apply(context, args);
        context = args = null;
      }
    }
  };

  var debounced = function(){
    // 缓存最近一次触发的执行上下文
    context = this;
    // 缓存最近一次触发的函数参数
    args = arguments;
    // 记录最近一次触发的时间戳
    timestamp = Date.now();
    // 是否立即执行,不用理会
    var callNow = immediate && !timeout;
    // 为了更加具体化,我们来模拟2次用户的操作
    // 分别是:0ms操作一次,10ms 操作一次
    // ----- 执行过程如下 -----
    // 0ms的操作,timeout 是 undifined,取反为真值,则执行 setTimeout(later, wait);
    // 10ms的操作,timeout 为上面那一步的 定时器id值,取反为假值,保存了上下文,参数,不做其它的处理
    // 这个设计非常巧妙,成功避开高频操作clearTimeout,带来的损耗
    // 聊了一会天,时间到了wait 的设定值,执行later(此时不考虑堵塞什么的)
    // 此时,我们看看later 函数的内部细节
    if (!timeout) timeout = setTimeout(later, wait);
    if (callNow) {
      result = func.apply(context, args);
      context = args = null;
    }

    return result;
  };
  return debounced;
};

节流的算法实现

  • 节流:从0开始计时,设置一个固定延时执行时间,此期间如果再触发,不再调整延时时间,到时间就执行最近一次触发函数。
  • 算法:开始计时,如果在此期间触发,则保存触发时的上下文、参数,时间到了,执行函数
function throttle (func, wait) {
  var ctx, args, rtn, timeoutID; // caching
  var last = 0;

  return function throttled () {
    ctx = this;
    args = arguments;
    var delta = new Date() - last;
    // 我们来模拟一个用户3次操作0ms 5ms 10ms, wait取值100ms
    // step1: 0ms操作:timeoutID 为undefined,取反值为真
    // step4: 10ms,只保存了上下文,参数,不做其它的处理,并继续等待时间,到了就执行call函数
    if (!timeoutID)
      // step2: 0ms操作:delta >= wait成立,执行call函数,timeoutID 被赋值定时器id
      if (delta >= wait) call();
       // step3: 5ms操作:执行timeoutID = setTimeout(call, wait - delta);
      else timeoutID = setTimeout(call, wait - delta);
    return rtn;
  };

  function call () {
    timeoutID = 0;
    last = +new Date();
    rtn = func.apply(ctx, args);
    ctx = null;
    args = null;
  }
}

2个解决方案有什么区别?

  • 防抖:执行的时间到受到用户的操作向后推移,时间的参考点是最近一次触发时间。
  • 节流:执行的时间是固定的,用户的高频操作不会影响之前的设置时间,到时间了,就执行。时间参考点是上一次的执行时间点

在开发中选择防抖还是节流?

根据上面的区别,2个方案都有各自的特点,应当根据实际的业务场景进行选择?

  • 中间过程只是获取过结果的一个必经之路,用户更关注最后的操作结果,可以等待更长的时间
    • input输入搜索
    • window的resize 事件
  • 中间过程一定要有具体的内容响应出来,要相对的实时响应。
    • 下拉滚动获取更多的内容

    • 鼠标移动的轨迹/一直按着mousedown

引用说明