资讯专栏INFORMATION COLUMN

JS - debounce(去抖) 和 throttle(节流)

Mike617 / 3064人阅读

摘要:多次连续事件触发动作最后一次触发之后的指定时间间隔执行回调函数预先设定一个执行周期,当调用动作的时刻大于等于执行周期则执行该动作,然后进入下一个新的时间周期。

定义

为了避免某个事件在较短的时间段内(称为 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...

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/105533.html

相关文章

  • JS中的函数去抖节流

    摘要:上段代码的一个问题是,事件会在定时器结束后被触发,因此会出现一定的延迟,如果想让事件被立即触发,可以使用以下的去抖函数但是,对于去抖来说,在某些场景下是不合适的,因此我们可以使用节流。 参考文章游戏星人眼中的节流与去抖(很生动) 函数去抖与节流 Debounce:函数去抖就是对于一定时间段的连续的函数调用,只让其执行一次Throttle:函数节流就是让连续执行的函数,变成固定时间段间断...

    fuchenxuan 评论0 收藏0
  • JS throttledebounce的区别

    摘要:可以看下面的栗子这个图中图中每个小格大约,右边有原生事件与节流去抖插件的与事件。即如果有连续不断的触发,每执行一次,用在每隔一定间隔执行回调的场景。执行啦打印执行啦打印执行啦节流按照上面的说明,节流就是连续多次内的操作按照指定的间隔来执行。 一般在项目中我们会对input、scroll、resize等事件进行节流控制,防止事件过多触发,减少资源消耗;在vue的官网的例子中就有关于lod...

    wawor4827 评论0 收藏0
  • 函数节流throttle)与函数去抖debounce

    摘要:去抖主要针对的是频繁触发某个事件后,然后进行后续处理的场景。常见的就是频繁输入停止假设后进行查询等操作。函数接口定义实际需要调用的函数空闲时间返回调用函数函数接口定义延迟时间需要调用的函数返回函数 前言 做过前端的童鞋应该都知道lodash这个强大的使用工具库。为什么要写这篇文章呢,主要今天遇到一个问题,socket推送消息太频繁,导致saga频繁更新,页面有所卡顿,需要通过函数节流控...

    bergwhite 评论0 收藏0
  • JavaScript 函数节流函数去抖应用场景辨析

    摘要:函数节流和去抖的出现场景,一般都伴随着客户端的事件监听。函数节流的核心是,让一个函数不要执行得太频繁,减少一些过快的调用来节流。 概述 也是好久没更新 源码解读,看着房价蹭蹭暴涨,心里也是五味杂陈,对未来充满恐惧和迷茫 ...(敢问一句你们上岸了吗) 言归正传,今天要介绍的是 underscore 中两个重要的方法,函数节流和函数去抖。这篇文章不会涉及具体的代码实现(关于代码实现请期...

    ZHAO_ 评论0 收藏0
  • js中函数节流&函数去抖

    摘要:节流保证在一定时间内,只能触发一次。我们在尝试一下去抖消抖,消除抖动,感觉这个更好听有没有什么现成的上的一次发现源码的经历以及对学术界拿来主义的思考函数节流和函数去抖应用场景辨析函数去抖的实现 开篇先提几个问题? 1.做搜索框的时候你使用什么事件?change?blur?keyup?你想要的效果是什么? 2.scroll事件怎么就触发?是滚一段距离触发一次?还是滚一圈触发一次?还是滚...

    王军 评论0 收藏0

发表评论

0条评论

Mike617

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<