函数节流(throttle)与函数去抖(debounce)

1 概念

首先讲讲关注函数节流这个概念的原因~~,其实还是因为业务驱动,在实现无限加载组件(就是滚动到底部,就可以加载更多)的时候,采用了监听scroll事件的方法,然后发现随便一滚动,这个scroll事件都会触发,然后经历一系列的计算,然后就想是不是需要降低scroll处理函数触发的次数。赶紧查资料,发现这玩意大神们都考虑过了,小弟赶紧总结一下。

首先,大家得理解什么是节流(throttle),什么是去抖(debounce)?

节流的概念说白了就是控制函数调用的次数,比如一些耗时的dom操作,不易非常快速的频繁调用,因此就需要在某些情况下,丢弃一些函数调用。什么时候丢弃呢?后面详细介绍(关于什么时候丢弃这个问题,就体现出了本人与大神思维的差距,容后详表)。
去抖的概念其实也很好理解,就是如果你快速的进行某些操作,那么实际上只需要执行最后一次操作。例如我们切换tab页,在快速的点击下,实际上我们需要的只是展示最后一次点击出现的页面,之前的点击我们就可以理解为其为抖动
实际上去抖也是节流的一种形式

2 函数节流

假设我们需要进行节流的函数为scroll事件的事件处理函数

function onScroll(e){
   // handle scroll
}

2.1 简单实现

首先我们介绍下简单的函数节流实现方法.

  • 方法1
function throttle(method,delay,context){
    clearTimeout(method.timerId);
    method.timerId = setTimeout(function(){
        method.call(context);
    },delay);
}
window.onscroll = throttle(onScroll,50,this)

思路很简单,将需要节流的函数延迟delay时间执行,如果在delay时间段内函数再次被调用,那么使用clearTimeout取消上一次调用。

  • 方法2
function throttle(method,delay){
    var timerId = null;
    return function(){
        var context = this,args = arguments;
        clearTimeout(timerId);
        timerId = setTimeout(function(){
            method.apply(context,args);
        },delay);
    };
}
window.onscroll = throttle(onScroll,50);

这种方法函数节流的方式实际上与上面相同,改进的是throttle方法返回一个函数,这个函数通过闭包保存了调用上下文this以及参数信息arguments,并使用apply方法使被调用函数保持正确的上下文和参数。

  • 方法3
function throttle(method,delay,mustRunDelay){
    var timerId = null,startTime = new Date();
    return function(){
        var context = this,args = arguments,currentTime = new Date();
        if(currentTime - startTime>=mustRunDelay){
            method.apply(context,args);
            startTime = currentTime;
        }else{
            timerId = setTimeout(function(){
                method.apply(context,args);
            },delay);
        }
    };
}
window.onscroll = throttle(onScroll,50,100);

上面两种方法有一个缺点,就是如果被节流函数的调用间隔都非常短的话,那么函数就一直无法被调用,只有最后一次调用能够成功(这实际上是去抖需要的功能)。
因此,为了被节流函数能够在一段时间保证被触发一次,我们添加一个mustRunDelay参数,并且记录函数每次执行的时间,如果两次的时间间隔(currentTime-startTime)大于mustRunDelay时,必须执行函数一次。这样就实现了在固定间隔内函数一定执行的功能,同时也丢弃了一些多频次低间隔触发的函数操作。

2.2 underscore的实现

仔细研究2.1简单实现中的节流方法,可以发现想法还是比较直线。跟大神考虑问题还有很大差距,总结了下问题如下:

  • delaymustRunDelay两个参数实际上是不必要的,而且会造成混淆,实际上丢弃间隔和执行间隔应该统一。为什么呢?我们可以思考节流的本质是什么呢?其本质实际上是为了使函数调用变得平滑,而我们的节流策略应该上就是需要一个时间间隔,在超出这个时间间隔的函数调用可以执行,在低于这个时间间隔内的函数调用要被丢弃。
  • 函数的调用都是使用setTimeout延时执行的,是否可以不延时执行?
  • 函数的第一次调用是否要触发?

underscore节流实现考虑了以上所说的问题,throttle函数接收单个参数func,wait,options,其中func为要执行的函数,wait为节流间隔,options是一个对象,可以接收leadingtrailing两个属性,leading属性表示第一次func调用是否立即执行,trailing属性表示func调用是立即执行还是使用setTimeout延时执行

  _.throttle = function(func, wait, options) {
    var context, args, result;
    var timeout = null;
    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;
    };
    return function() {
      var now = _.now();
      // 如果是第一次执行函数且leading为false,那么就延时执行
      if (!previous && options.leading === false) previous = now;
      var remaining = wait - (now - previous);
      context = this;
      args = arguments;
      // 如果剩余时间<0,或者由于机器原因,导致时间计算不正确(remaining肯定应在0到wait之内),那么进行函数调用
      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;
    };
  };

3 函数去抖

3.1 underscore的实现

underscoredebounce的实现思路与上文的简单方法2相似,不同的是添加了immediate参数,用来指定函数调用是立即执行还是是延时执行。

  _.debounce = function(func, wait, immediate) {
    var timeout, args, context, timestamp, result;

    var later = function() {
      var last = _.now() - timestamp;
      // 每次调用都会更新timestamp的值,如果时间间隔没有超过wait,那么启用新的定时器
      if (last < wait && last >= 0) {
        timeout = setTimeout(later, wait - last);
      } else {
        timeout = null;
        if (!immediate) {
          result = func.apply(context, args);
          if (!timeout) context = args = null;
        }
      }
    };

    return function() {
      context = this;
      args = arguments;
      timestamp = _.now();
      // 立即执行
      var callNow = immediate && !timeout;
      if (!timeout) timeout = setTimeout(later, wait);
      if (callNow) {
        result = func.apply(context, args);
        context = args = null;
      }

      return result;
    };
  };

3 使用场景

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

  • 文本输入、自动完成,keyup 事件, 需要debounce
  • 鼠标移动,mousemove 事件,需要throttle
  • DOM 元素动态定位,window 对象的resizescroll 事件,需要throttle
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容

  • JavaScript-性能优化,函数节流(throttle)与函数去抖(debounce)JS魔法堂:函数节流(t...
    大前端之路阅读 183评论 0 0
  • 实际上啊,今天早上本来想干点别的,但是在吃早餐的时候浏览了下掘金,然后看到了这篇博文 Jake的关于图片懒加载 ...
    6J阅读 9,867评论 1 23
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,633评论 18 139
  • 1月1日 凌晨一点五十六睡下,早上七点半准时自然,这证明我睡眠充足只需要六个小时。以后只要睡够六小时就可以。 按照...
    窗外依然阅读 383评论 0 0
  • 她坐在我对面,夹着一根细长的香烟,眼神走向烟雾的末端:"他在外面有多少女人我都不管,只要准时拿钱回家,还有,别把什...
    江一月阅读 361评论 1 0