JavaScript中的简单节流

125

我正在寻找一个简单的JavaScript节流函数。我知道类库,如lodash和underscore有它,但是仅为一个函数,包含这些库将过度臃肿。

我也查看了jQuery是否有类似的函数-没有找到。

我已经找到了一个可用的节流函数,以下是代码:

function throttle(fn, threshhold, scope) {
  threshhold || (threshhold = 250);
  var last,
      deferTimer;
  return function () {
    var context = scope || this;

    var now = +new Date,
        args = arguments;
    if (last && now < last + threshhold) {
      // hold on to it
      clearTimeout(deferTimer);
      deferTimer = setTimeout(function () {
        last = now;
        fn.apply(context, args);
      }, threshhold);
    } else {
      last = now;
      fn.apply(context, args);
    }
  };
}
这个问题在于:它会在间隔时间结束后再次触发函数。假设我设置了一个按键每 10 秒触发一次的节流器,如果我按下按键两次,当 10 秒结束时它仍然会触发第二次按键。我不希望这种行为发生。

3
  1. jQuery有一个插件http://benalman.com/projects/jquery-throttle-debounce-plugin/
  2. 为什么不直接使用underscore/lodash的节流实现?
- Oleg
@Oleg,是否可以仅使用油门而不导入整个库? - Mia
1
你能设置一个示例,或者至少更好地解释一下使用情况吗?通常,按键节流非常简单设置,类似于这样 -> http://jsfiddle.net/a3w6pLbj/1/ - adeneo
1
https://github.com/jashkenas/underscore/blob/master/underscore.js#L754 - Oleg
1
如果你的打包工具没有tree-shaking功能,你仍然可以像这样使用导入:import throttle from 'lodash/throttle'。这样,只会导入一个函数。 - Shivam Jha
24个回答

143

我会使用underscore.jslodash源代码来查找这个函数的经过充分测试的版本。

这是略微修改后的underscore代码版本,删除了所有对underscore.js本身的引用:

// Returns a function, that, when invoked, will only be triggered at most once
// during a given window of time. Normally, the throttled function will run
// as much as it can, without ever going more than once per `wait` duration;
// but if you'd like to disable the execution on the leading edge, pass
// `{leading: false}`. To disable execution on the trailing edge, ditto.
function throttle(func, wait, options) {
  var context, args, result;
  var timeout = null;
  var previous = 0;
  if (!options) options = {};
  var later = function() {
    previous = options.leading === false ? 0 : Date.now();
    timeout = null;
    result = func.apply(context, args);
    if (!timeout) context = args = null;
  };
  return function() {
    var now = Date.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;
  };
};

请注意,如果您不需要underscore支持的所有选项,则可以简化此代码。

下面是这个函数的非常简单且不可配置的版本:

function throttle (callback, limit) {
    var waiting = false;                      // Initially, we're not waiting
    return function () {                      // We return a throttled function
        if (!waiting) {                       // If we're not waiting
            callback.apply(this, arguments);  // Execute users function
            waiting = true;                   // Prevent future invocations
            setTimeout(function () {          // After a period of time
                waiting = false;              // And allow future invocations
            }, limit);
        }
    }
}

编辑1:根据@Zettam的评论,删除了对下划线的另一个引用

编辑2:根据@lolzery @wowzery的评论,添加有关lodash和可能的代码简化的建议

编辑3:由于广大用户的要求,我从@vsync的评论中修改了一个非常简单且不可配置的版本的函数


68
对我而言,这看起来并不简单。这是一个简单的好例子。点击链接查看。 - vsync
12
的确,这并不简单。但它已经准备好并且是开源的。 - Clément Prévost
18
这不像 @vsync 提供的那个简单,其中一个原因是因为它支持尾随调用。使用这个方法,如果你调用结果函数两次,它将导致对包装函数的两次调用:一次是立即执行的,另一次是在延迟后执行的。而在 vsync 提供的那个方法中,它会导致一个立即执行的调用,但延迟后不会再有调用。在许多情况下,接收尾随调用非常重要,以便获取最后一个视口大小或任何你正在尝试做的事情。 - Aaronius
3
请勿使用此代码。我并非傲慢,只是希望更加实用。这个答案比必要的复杂得多。我已经发布了一个单独的答案,用更少的代码实现了所有功能,并且还有更多功能。 - Jack G
4
@Nico,“arguments”对象在任何非箭头函数中都是已定义的:https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Functions/arguments。 - Clément Prévost
显示剩余15条评论

35

这个怎么样?

function throttle(func, timeFrame) {
  var lastTime = 0;
  return function () {
      var now = Date.now();
      if (now - lastTime >= timeFrame) {
          func();
          lastTime = now;
      }
  };
}

简单。

你可能会对查看源代码感兴趣。


3
这是页面上最干净、最简单的实现。 - Lawrence Dol
4
对我来说,这只能使用 Date.now() 而不是 new Date() - Ian Jones
我在进行 now - lastTime 时,TS 还会发出警告,因为 now 是一个日期。用 Date.now() 替换它以获取一个数字似乎是合理的。 - Vadorequest
与使用 setTimeout 相比,这种方法有什么不足之处吗? - Vic
2
@Vic,不能保证你的函数的最后一次调用。 - Ricky Boyce
我们如何确保在 func 没有被调用的情况下,最终也会被调用?例如,timeFrame 是 5000,但你在 3000 内调用了两次;第一次调用已经处理了,但第二次被跳过了;我们如何自动调用 func 来处理第二次调用? - daCoda

14

回调函数:接受应该被调用的函数

限制次数:在时间限制内调用函数的次数

时间跨度:重置限制次数的时间跨度

功能和用法:假设您有一个API,允许用户在1分钟内调用它10次

function throttling(callback, limit, time) {
    /// monitor the count
    var calledCount = 0;

    /// refresh the `calledCount` varialbe after the `time` has been passed
    setInterval(function(){ calledCount = 0 }, time);

    /// creating a closure that will be called
    return function(){
        /// checking the limit (if limit is exceeded then do not call the passed function
        if (limit > calledCount) {
            /// increase the count
            calledCount++;
            callback(); /// call the function
        } 
        else console.log('not calling because the limit has exceeded');
    };
}
    
//////////////////////////////////////////////////////////// 
// how to use

/// creating a function to pass in the throttling function 
function cb(){
    console.log("called");
}

/// calling the closure function in every 100 milliseconds
setInterval(throttling(cb, 3, 1000), 100);


6
你的回答并不比必要的复杂程度少。 - Denny
3
我建议这样触发回调函数:callback(...arguments),以保留原始的参数。非常方便。 - vsync
这应该是被接受的答案。简单易懂。 - Gaurang Tandon
@Denny 这个答案浪费了大量的浏览器资源。每次创建单个处理程序时,它都会启动一个全新的间隔函数。即使在删除事件侦听器之后,持续轮询也会耗尽计算机资源,导致高内存使用率、卡顿和页面砖块化。 - Jack G
6
请勿在生产代码中使用此答案。这是糟糕编程的极致体现。假设您的页面上有1000个按钮(听起来可能很多,但请再想一想:按钮随处可见:在弹出窗口、子菜单、面板等中),并希望将每个按钮的触发次数限制为200秒内最多一次。由于它们可能会同时启动,每333毫秒(或3次/秒),当所有这些定时器需要重新检查时,就会出现巨大的延迟峰值。此答案完全滥用了setInterval不应该用于的目的。 - Jack G

12

我刚需要一个用于窗口大小调整事件的节流/防抖函数,并且出于好奇,我也想知道它们是什么以及它们是如何工作的。

我已经阅读了多篇关于此的博客文章和SO上的问答,但它们似乎都过于复杂化、建议使用库或仅提供描述而不是简单的纯JS实现。

由于描述已经很丰富,因此这里是我的实现:

function throttle(callback, delay) {
    var timeoutHandler = null;
    return function () {
        if (timeoutHandler == null) {
            timeoutHandler = setTimeout(function () {
                callback();
                timeoutHandler = null;
            }, delay);
        }
    }
}

function debounce(callback, delay) {
    var timeoutHandler = null;
    return function () {
        clearTimeout(timeoutHandler);
        timeoutHandler = setTimeout(function () {
            callback();
        }, delay);
    }
}

这些可能需要微调(例如,最初回调不会立即被调用)。

看一下实际效果的区别(尝试调整窗口大小):

function throttle(callback, delay) {
    var timeoutHandler = null;
    return function () {
        if (timeoutHandler == null) {
            timeoutHandler = setTimeout(function () {
                callback();
                timeoutHandler = null;
            }, delay);
        }
    }
}

function debounce(callback, delay) {
    var timeoutHandler = null;
    return function () {
        clearTimeout(timeoutHandler);
        timeoutHandler = setTimeout(function () {
            callback();
        }, delay);
    }
}

var cellDefault  = document.querySelector("#cellDefault div");
var cellThrottle = document.querySelector("#cellThrottle div");
var cellDebounce = document.querySelector("#cellDebounce div");

window.addEventListener("resize", function () {
    var span = document.createElement("span");
    span.innerText = window.innerWidth;
    cellDefault.appendChild(span);
    cellDefault.scrollTop = cellDefault.scrollHeight;
});

window.addEventListener("resize", throttle(function () {
    var span = document.createElement("span");
    span.innerText = window.innerWidth;
    cellThrottle.appendChild(span);
    cellThrottle.scrollTop = cellThrottle.scrollHeight;
}, 500));

window.addEventListener("resize", debounce(function () {
    var span = document.createElement("span");
    span.innerText = window.innerWidth;
    cellDebounce.appendChild(span);
    cellDebounce.scrollTop = cellDebounce.scrollHeight;
}, 500));
table {
    border-collapse: collapse;
    margin: 10px;
}
table td {
    border: 1px solid silver;
    padding: 5px;
}
table tr:last-child td div {
    width: 60px;
    height: 200px;
    overflow: auto;
}
table tr:last-child td span {
    display: block;
}
<table>
    <tr>
        <td>default</td>
        <td>throttle</td>
        <td>debounce</td>
    </tr>
    <tr>
        <td id="cellDefault">
            <div></div>
        </td>
        <td id="cellThrottle">
            <div></div>
        </td>
        <td id="cellDebounce">
            <div></div>
        </td>
    </tr>
</table>

JSFiddle


设计不良,会导致附着的所有内容都出现很大的延迟,使得网站对用户不响应。 - Jack G
4
@commonSenseCode“ does not even work ”是什么意思?我已经提供了演示代码。它显然是有效的。请尽量详细解释一下。无论什么地方出了问题,我相信这与您的实现有些关系。 - akinuri
在 throttle() 中的 clearInterval() 调用没有意义,因为 setTimeout() 应该与 clearTimeout() 配对,而且在计时器已经触发后取消计时器是毫无意义的。另外,我希望第一个回调函数能够立即执行,而后续的回调函数能够延迟执行,但根据您的需求,也许延迟执行第一个回调函数也可以是好的或坏的。 - Bill Keese
@BillKeese 哦,你说得对。在节流函数中没有必要清除超时。我猜这是从复制/粘贴/编辑去抖动函数的过程中遗留下来的。至于立即调用,那可以进行微调。我不想用额外的功能使函数变得复杂。我喜欢那句话“一张图片胜过千言万语”,所以我想提供简单的演示。 - akinuri

12

如果不使用几乎成为标准的 lodash 中的 throttle 是因为希望包或捆绑包大小更小,那么可以仅将 throttle 包含在您的捆绑包中,而不是整个 lodash 库。例如,在 ES6 中,可以这样实现:

import throttle from 'lodash/throttle';

此外,lodash 还提供了一个名为 lodash.throttle 的仅包含 throttle 方法的包,在 ES6 中可以通过简单的 import 命令,在 ES5 中可以通过 require 命令来使用。


5
查看了这段代码,它使用了两个文件的导入,因此对于一个简单的节流函数,你需要三个文件。我认为这有点过度设计,特别是如果有人(比如我自己)只需要在大约200行代码的程序中使用一个节流函数。 - vsync
6
是的,它在内部使用了“debounce”和“isObject”,整个捆绑包大小约为2.1KB压缩。我想,对于一个小程序来说可能没有意义,但是在大型项目中,我更喜欢使用它而不是创建自己的节流函数,这也需要进行测试 :) - Divyanshu Maithani
对于现代项目,建议使用 lodash-es 而不是 lodash - dlq

10

以下是我在ES6中实现节流函数的方式,共9行代码。希望可以帮助到你。

function throttle(func, delay) {
  let timeout = null
  return function(...args) {
    if (!timeout) {
      timeout = setTimeout(() => {
        func.call(this, ...args)
        timeout = null
      }, delay)
    }
  }
}

点击这个链接查看它是如何工作的。


2
简单但相当低效:即使不适合,它也会延迟函数,并且它不会保持待处理事件的新鲜度,可能导致用户交互滞后。此外,使用...扩展语法是不恰当的,因为只有一个参数被传递给事件监听器:事件对象。 - Jack G
6
使用 spread 运算符并不是不恰当的;没有限制 throttle 函数只能用于事件处理程序。 - Lawrence Dol
这将触发初始调用的 ...args,而不是最新的调用。 - Izhaki

6

我在这里看到了很多对于“JavaScript 中的简单节流”过于复杂的回答。

几乎所有更简单的答案都会忽略 “在节流中调用的函数”,而不是延迟执行到下一个时间间隔。

这里提供了一个简单的实现,同时也处理了在节流中调用的函数:

const throttle = (func, limit) => {
  let lastFunc;
  let lastRan = Date.now() - (limit + 1); //enforces a negative value on first run
  return function(...args) {
    const context = this;
    clearTimeout(lastFunc);
    lastFunc = setTimeout(() => {
      func.apply(context, args);
      lastRan = Date.now();
    }, limit - (Date.now() - lastRan)); //negative values execute immediately
  }
}

这几乎是一个简单的防抖实现的完全相同版本。只需添加一个超时延迟的计算,需要跟踪上次运行函数的时间。请参见下面:

const debounce = (func, limit) => {
  let lastFunc;
  return function(...args) {
    const context = this;
    clearTimeout(lastFunc);
    lastFunc = setTimeout(() => {
      func.apply(context, args)
    }, limit); //no calc here, just use limit
  }
}

3

ES6 中的简单解决方案。 Codepen 演示

const handleOnClick = () => {
  console.log("hello")
}

const throttle = (func, delay) => {
  let timeout = null;

  return function (...args) {
    if (timeout === null) {
      func.apply(this, args);
      
      timeout = setTimeout(() => {
        timeout = null;
      }, delay)
    }
  }
}

document.querySelector("#button").addEventListener("click", throttle(handleOnClick, 500))
<button type="button" id="button">Click me</button>


2

这是我对Vikas的文章进行的改版

throttle: function (callback, limit, time) {
    var calledCount = 0;
    var timeout = null;

    return function () {
        if (limit > calledCount) {
            calledCount++;
            callback(); 
        }
        if (!timeout) {
            timeout = setTimeout(function () {
                calledCount = 0
                timeout = null;
            }, time);
        }
    };
}

我发现使用 setInterval 不是一个好主意。

2

使用前后调用:

const throttle = (fn, ms) => {
  let locked = false

  return function () {
    if (!locked) {
      locked = true
      fn.apply(this, arguments)

      setTimeout(() => {
        fn.apply(this, arguments)
        locked = false
      }, ms)
    }
  }
}

测试用例:

function log({ gender, address }) {
  console.log({
    name: this.name,
    gender,
    address,
  })
}

const jack = {
  name: 'Jack',
  log: throttle(log, 3000),
}

Array.from({ length: 5 }, () => jack.log({ gender: 'Male', address: 'LA' }))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接