乐趣区

JS-debounce去抖-和-throttle节流

定义

为了避免某个事件在较短的时间段内(称为 T)内连续触发从而引起的其对应的事件处理函数不必要的连续执行的一种事件处理机制(高频触发事件解决方案)
debounce:当调用动作触发一段时间后,才会执行该动作,若在这段时间间隔内又调用此动作则将重新计算时间间隔。(多次连续事件触发动作 / 最后一次触发之后的指定时间间隔执行回调函数)
throttle:预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新的时间周期。(每个指定时间执行一次回调函数,可以指定时间间隔之前调用)

区别

1、throttle 保证了在每个 T 内至少执行一次,而 debounce 没有这样的保证
2、每次事件触发时参考的 时间点 ,对于debounce 来是 上一次事件触发的时间 并且在延时没有结束时会重置延时;
throttle 上一次 handler 执行的时间 并且在延时尚未结束时不会重置延时

影响

响应速度跟不上触发频率,往往会出现延迟,导致假死或者卡顿感

实现

去抖 debounce

空闲控制:所有操作最后一次性执行

【简洁版】

/**
* @param fn {Function}   实际要执行的函数
* @param delay {Number}  延迟时间,也就是阈值,单位是毫秒(ms)* @return {Function}     返回一个“去弹跳”了的函数
*/
function debounce(fn, delay) {
  // 定时器,用来 setTimeout
  var timer

  // 返回一个函数,这个函数会在一个时间区间结束后的 delay 毫秒时执行 fn 函数
  return function () {
    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments

    // 每次这个返回的函数被调用,就清除定时器,以保证不执行 fn
    clearTimeout(timer)

    // 当返回的函数被最后一次调用后(也就是用户停止了某个连续的操作),// 再过 delay 毫秒就执行 fn
    timer = setTimeout(function () {fn.apply(context, args)
    }, delay)
  }
}

【完整版】

// immediate:是否立即执行回调函数; 其它参数同上
function debounce(fn, wait, immediate) {
    let timer = null

    return function() {let args = [].slice.call(arguments)

        if (immediate && !timer) {fn.apply(this, args)
        }

        if (timer) clearTimeout(timer)
        timer = setTimeout(() => { // 箭头函数,this 指向外层环境
            fn.apply(this, args)
        }, wait)
    }
}
// 测试:var fn = function() {console.log('debounce..')
}
oDiv.addEventListener('click', debounce(fn, 3000))

节流 throttle

固定频次:减少执行频次,每隔一定时间执行一次

【简洁版】

/**
* 固定回调函数执行的频次
* @param fn {Function}   实际要执行的函数
* @param interval {Number}  执行间隔,单位是毫秒(ms)*
* @return {Function}     返回一个“节流”函数
*/
var throttle = function (fn, interval) {
  // 记录前一次时间
  var last = +new Date()
  var timer = null
  // 包装完后返回 闭包函数
  return function () {var current = +new Date()
    var args = [].slice.call(arguments, 0)
    var context = this
    // 首先清除定时器
    clearTimeout(timer)
    // current 与 last 间隔大于 interval 执行一次 fn
    // 在一个周期内 last 相对固定 current 一直再增加
    // 这里可以保证调用很密集的情况下 current 和 last 必须是相隔 interval 才会调用 fn
    if (current - last >= interval) {fn.apply(context, args)
      last = current
    } else {
      // 如果没有大于间隔 添加定时器
      // 这可以保证 即使后面没有再次触发 fn 也会在规定的 interval 后被调用
      timer = setTimeout(function() {fn.apply(context, args)
        last = current
      }, interval-(current - last))
    }
  }
}

【完整版】

/**
 * 频率控制 返回函数连续调用时,func 执行频率限定为 次 / wait
 * 自动合并 data
 * 
 * 若无 option 选项,或者同时为 true,即 option.trailing !== false && option.leading !== false,在固定时间开始时刻调用一次回调,并每个固定时间最后时刻调用回调
 * 若 option.trailing !== false && option.leading === false, 每个固定时间最后时刻调用回调
 * 若 option.trailing === false && option.leading  !== false,  只会在固定时间开始时刻调用一次回调
 * 若同时为 false 则不会被调用
 *
 * @param  {function}   func      传入函数
 * @param  {number}     wait      表示时间窗口的间隔
 * @param  {object}     options   如果想忽略开始边界上的调用,传入{leading: false}。默认 undefined
 *                                如果想忽略结尾边界上的调用,传入{trailing: false},默认 undefined
 * @return {function}             返回客户调用函数
 */
function throttle (func, wait, options) {
  var context, args, result;
  var timeout = null;
  // 上次执行时间点
  var previous = 0;
  if (!options) {options = {}; }
  // 延迟执行函数
  function later () {
    // 若设定了开始边界不执行选项,上次执行时间始终为 0
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) {context = args = null;}
  }
  return function (handle, data) {var now = Date.now();
    // 首次执行时,如果设定了开始边界不执行选项,将上次执行时间设定为当前时间。if (!previous && options.leading === false) {previous = now;}
    // 延迟执行时间间隔
    var remaining = wait - (now - previous);
    context = this;
    args = args ? [handle, Object.assign(args[1], data)] : [handle, data];
    // 延迟时间间隔 remaining 小于等于 0,表示上次执行至此所间隔时间已经超过一个时间窗口
    // remaining 大于时间窗口 wait,表示客户端系统时间被调整过
    if (remaining <= 0 || remaining > wait) {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
  }
}

运用

  • 游戏射击,keydown 事件
  • 文本输入、自动完成,keyup 事件
  • 鼠标移动,mousemove 事件
  • DOM 元素动态定位,window 对象的 resize 和 scroll 事件

前两者 debounce 和 throttle 都可以按需使用;后两者肯定是用 throttle

underscore 实现源码

debounce

_.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;
};

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;
};

【参考】
https://blog.coding.net/blog/…
https://github.com/lishengzxc…

退出移动版