Javascript: JS函数节流与防抖 throttle,debounce

throttle 与 debounce 都是为了限制函数的执行频次,以优化函数触发频率过高导致的响应速度跟不上触发频率,出现延迟,假死或卡顿的现象。

概念

throttle:连续的时间间隔(每隔一定时间间隔执行callback)。
debounce:空闲的时间间隔(callback执行完,过一定空闲时间间隔再执行callback)。

电梯超时

想象每天上班大厦底下的电梯。把电梯完成一次运送,类比为一次函数的执行和响应。假设电梯有两种运行策略 throttle 和 debounce ,超时设定为15秒,不考虑容量限制。
throttle 策略的电梯。保证如果电梯第一个人进来后,15秒后准时运送一次,不等待。如果没有人,则待机。
debounce 策略的电梯。如果电梯里有人进来,等待15秒。如果又人进来,15秒等待重新计时,直到15秒超时,开始运送。

<div id="switcher">
    <div id="actions">
      <button id="use-lodash" data-lodash=false>Using <strong>underscore.js</strong> | <strike>lodash.js</strike></button>
      <button id="every100">1 event every 100 ms</button>
      <button id="every300" class="every">2 events every 300 ms</button>
    </div>
  </div>

    <div id="sidebar">
      <div id="sidebar-free">Move your mouse here</div>
    </div>

    <div id="content">
      <h2>Mousemove Events:</h2><div id="allEvents"></div>
      <h2>Debounce Inmediate: _.debounce(fn, 200, true);</h2><div id="debounce_true"></div>
      <h2>Debounce Inmediate: $.debounce(200, true, fn);</h2> <div id="debouncejtrue"></div>
      <h2>Debounce: _.debounce(fn, 200);</h2> <div id="debounce_false"></div>
      <h2>Debounce: $.debounce(200, false, fn);</h2> <div id="debouncejfalse"></div>
      <h2>Throttle with trailing: _.throttle(fn, 200);</h2> <div id="throttle_true"></div>
      <h2>Throttle with trailing: $.throttle(200, false, fn);</h2> <div id="throttlejfalse"></div>
      <h2>Throttle: $.throttle(200, true, fn);</h2> <div id="throttlejtrue"></div>
    </div>
    <a href="https://github.com/dcorb/debounce-throttle">
      <img style="position: absolute; top: 0; right: 0; border: 0;" src="https://s3.amazonaws.com/github/ribbons/forkme_right_white_ffffff.png" alt="Fork me on GitHub">
    </a>

<style>
  body {
        background: #444444;
        color: white;
        font: 15px/1.51 Helvetica, sans-serif;
        overflow:hidden;
      }
      #switcher {
        text-align:center;
      }
      button {
        cursor: pointer;
        color: black;
        border: 1px solid #999;
        background: #DADADA;
        padding: 2px 8px;
        -moz-border-radius: 4px;
        -webkit-border-radius: 4px;
        border-radius: 4px;
        background-image: -moz-linear-gradient(top,#EBEBEB,#B8B8B8);
        background-image: -o-linear-gradient(top,#EBEBEB,#B8B8B8);
        background-image: -webkit-gradient(linear,left top,left bottom,from(#EBEBEB),to(#B8B8B8));
        background-image: -webkit-linear-gradient(top,#EBEBEB,#B8B8B8);
        background-image: linear-gradient(top,#EBEBEB,#B8B8B8);
      }
      #use-lodash {
        margin-right: 30px;
      }
      #content {
        margin-left: 145px;
        position: relative;
      }
      #content span {
        height:17px;
        width:7px;
        vertical-align:top;
        display:inline-block;
        border-left:1px solid #999;
        font-size:12px;
      }
      #content div{
        margin:0;
        font-family:monospace;
        color: black;
        height: 23px;
      }
      h2 {
        margin:0;
        height: 15px;
        clear:both;
        font-weight: normal;
        width:100%;
        font-size:11px;
      }
      #sidebar {
        height:100%;
        width: 120px;
        position:absolute;
      }
      #sidebar-free {
        height: 197px;
        width: 100%;
        border: 1px solid #ccc;
        position:relative;
        padding: 55px 5px;
        text-align: center;
        background-color: #3e5f7c;
      }

      .color0 { background-color: #9BFFBB}
      .color1 { background-color: #E3FF7E}
      .color2 { background-color: #B9C6FF}
      .color3 { background-color: #99FF7E}
      .color4 { background-color: #FFB38A}
      .color5 { background-color: #A5FCFF}
      .color6 { background-color: #FF8E9B}
      .color7 { background-color: #FFE589}
      .color8 { background-color: #FFA3D8}
      .color9 { background-color: #5ca6ff}
</style>
<script>
$(document).ready(function(){

  var allEvents = $('#allEvents'),
     divDebounce_true = $('#debounce_true'),
     divDebouncejtrue = $('#debouncejtrue'),
     divDebounce_false = $('#debounce_false'),
     divDebouncejfalse = $('#debouncejfalse'),
     divThrottle_true = $('#throttle_true'),
     divThrottlejtrue = $('#throttlejtrue'),
     divThrottlejfalse = $('#throttlejfalse'),
     sidebar_mousemove = $('#sidebar-free'),
     counter = 0,
     next_color = 0,
     drawing,
     drawing_automated,
     lazy_Debounce_Events,
     lazyDebounce_true,
     lazyDebouncejtrue,
     lazyDebounce_false,
     lazyDebouncejfalse,
     lazyThrottle_true,
     lazyThrottlejtrue,
     lazyThrottlejfalse;


  function update(div, color){
    div[0].lastChild.className = 'color' + color;
    div[0].lastChild.innerHTML = color;
  }

  function updateEvents(){
    update(allEvents, next_color);
    lazyDebounce_true(divDebounce_true, next_color);
   
    lazyDebounce_false(divDebounce_false, next_color);
   
    lazyThrottle_true(divThrottle_true, next_color);
   
    next_color++;
    if (next_color > 9){
      next_color = 0;
    }
  }

  function setup_lazy_functions(_){
    lazy_Debounce_Events = _.throttle(updateEvents, 50);

    lazyDebounce_true = _.debounce(update, 200, true);

    lazyDebounce_false = _.debounce(update, 200, false);

    lazyThrottle_true = _.throttle(update, 200);

  }


  // Initially demo it with underscore.js
  setup_lazy_functions(_);

  function reset(){
    allEvents.html('<span></span>');
    divDebounce_true.html('<span></span>');
    divDebouncejtrue.html('<span></span>');
    divDebounce_false.html('<span></span>');
    divDebouncejfalse.html('<span></span>');
    divThrottle_true.html('<span></span>');
    divThrottlejtrue.html('<span></span>');
    divThrottlejfalse.html('<span></span>');
    next_color = 0;
    counter = 0;
    clearInterval(drawing_automated);
    clearInterval(drawing);
  }

  sidebar_mousemove.on('mousemove', function (){
    lazy_Debounce_Events();
  });

  sidebar_mousemove.on('mouseenter', function(){
    reset();
    draw();
  });

  $('#every100').on('click', function(e){
    e.preventDefault();
    reset();
    draw();
    drawing_automated = setInterval(function(){
      sidebar_mousemove.trigger('mousemove');
    }, 100);
  });


  $('#every300').on('click', function(e){
    e.preventDefault();
    reset();
    draw();
    drawing_automated = setInterval(function(){
      sidebar_mousemove.trigger('mousemove');
      sidebar_mousemove.trigger('mousemove');
    }, 300);
   });


  $('#use-lodash').on('click', function(e){
    e.preventDefault();
    if ($(this).data('lodash')){
      setup_lazy_functions(_);
      $(this).data('lodash', false)
             .html('Using <strong>underscore.js</strong> | <del>lodash.js</del>')
    } else {
      setup_lazy_functions(lo);
      $(this).data('lodash', true)
             .html('Using <del>underscore.js</del> | <strong>lodash.js</strong>')
    }
  });

  var draw = function(){
    drawing = setInterval(function(){
      counter++;
      allEvents[0].appendChild(document.createElement('span'));
      divDebounce_true[0].appendChild(document.createElement('span'));
      divDebouncejtrue[0].appendChild(document.createElement('span'));
      divDebounce_false[0].appendChild(document.createElement('span'));
      divDebouncejfalse[0].appendChild(document.createElement('span'));
      divThrottlejtrue[0].appendChild(document.createElement('span'));
      divThrottlejfalse[0].appendChild(document.createElement('span'));
      divThrottle_true[0].appendChild(document.createElement('span'));

      if (counter > 95){
        clearInterval(drawing);
        clearInterval(drawing_automated);
      }
    }, 30);
  };

});
</script>

 

注意到上面的运行结果,第一行 Mousemove Events 展示了 mousemove 事件触发的频率。第二行和第三行是分别使用 underscore 与 jQuery 的 debounce 方法后事件的触发频率。第四、五行则是增加了一个 delay 参数后的触发频率。与之对比的是最后三行,使用的是 throttle 方法。

debounce的简单封装

在使用中,如果不用到Underscore.js库,那么我们可以自己封装一个throttle与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)
  }
}

其实思路很简单, debounce 返回了一个闭包,这个闭包依然会被连续频繁地调用,但是在闭包内部,却限制了原始函数 fn 的执行,强制 fn 只在连续操作停止后只执行一次。

debounce 的使用方式如下:

function ajax_lookup( event ) {
  // 对输入的内容$(this).val()执行 Ajax 查询
};
// 字符输入的频率比你预想的要快,Ajax 请求来不及回复。
$('input:text').keyup( ajax_lookup );
// 当用户停顿250毫秒以后才开始查找
$('input:text').keyup( debounce( ajax_lookup. 250 ) );

throttle的简单封装

/**
*
* @param fn {Function}   实际要执行的函数
* @param delay {Number}  执行间隔,单位是毫秒(ms)
*
* @return {Function}     返回一个“节流”函数
*/
function throttle(fn, threshhold) {
  // 记录上次执行的时间
  var last
  // 定时器
  var timer
  // 默认间隔为 250ms
  threshhold || (threshhold = 250)
  // 返回的函数,每过 threshhold 毫秒就执行一次 fn 函数
  return function () {
    // 保存函数调用时的上下文和参数,传递给 fn
    var context = this
    var args = arguments
    var now = +new Date()
    // 如果距离上次执行 fn 函数的时间小于 threshhold,那么就放弃
    // 执行 fn,并重新计时
    if (last && now < last + threshhold) {
      clearTimeout(timer)
      // 保证在当前时间区间结束后,再执行一次 fn
      timer = setTimeout(function () {
        last = now
        fn.apply(context, args)
      }, threshhold)
    // 在时间区间的最开始和到达指定间隔的时候执行一次 fn
    } else {
      last = now
      fn.apply(context, args)
    }
  }
}

原理也不复杂,相比 debounce ,无非是多了一个时间间隔的判断,其他的逻辑基本一致。
throttle 的使用方式如下:

function log( event ) {
  console.log( $(window).scrollTop(), event.timeStamp );
};
// 控制台记录窗口滚动事件,触发频率比你想象的要快
$(window).scroll( log );
// 控制台记录窗口滚动事件,每250ms最多触发一次
$(window).scroll( throttle( log, 250 ) );

其它实现

下面是 愚人码头 对throttle与debounce的实现

/*
* 频率控制 返回函数连续调用时,fn 执行频率限定为每多少时间执行一次
* @param fn {function}  需要调用的函数
* @param delay  {number}    延迟时间,单位毫秒
* @param immediate  {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。
* @return {function}实际调用函数
*/
var throttle = function (fn,delay, immediate, debounce) {
   var curr = +new Date(),//当前事件
       last_call = 0,
       last_exec = 0,
       timer = null,
       diff, //时间差
       context,//上下文
       args,
       exec = function () {
           last_exec = curr;
           fn.apply(context, args);
       };
   return function () {
       curr= +new Date();
       context = this,
       args = arguments,
       diff = curr - (debounce ? last_call : last_exec) - delay;
       clearTimeout(timer);
       if (debounce) {
           if (immediate) {
               timer = setTimeout(exec, delay);
           } else if (diff >= 0) {
               exec();
           }
       } else {
           if (diff >= 0) {
               exec();
           } else if (immediate) {
               timer = setTimeout(exec, -diff);
           }
       }
       last_call = curr;
   }
};
 
/*
* 空闲控制 返回函数连续调用时,空闲时间必须大于或等于 delay,fn 才会执行
* @param fn {function}  要调用的函数
* @param delay   {number}    空闲时间
* @param immediate  {bool} 给 immediate参数传递false 绑定的函数先执行,而不是delay后后执行。
* @return {function}实际调用函数
*/
 
var debounce = function (fn, delay, immediate) {
   return throttle(fn, delay, immediate, true);
};

使用场景

要牵涉到连续事件或频率控制相关的应用都可以考虑到这两个函数,比如:

  • input 中输入文字自动发送 ajax 请求进行自动补全: debounce
  • resize window 重新计算样式或布局:debounce
  • scroll 时更新样式,如随动效果:throttle
    最重要的还是理解两者对调用时间及次数上的处理,根据业务逻辑选择最合适的优化方案!

参考

css-tricks
javascript函数的throttle和debounce

 

本文:Javascript: JS函数节流与防抖 throttle,debounce

发表评论