简单的油门在js

时间:2014-11-22 14:09:07

标签: javascript jquery

我在JS中寻找一个简单的油门。我知道像lodash和下划线这样的库有它,但只有一个函数包含任何这些库都是过度的。

我还在检查jquery是否有类似的功能 - 找不到。

I have found one working throttle,这是代码:

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秒发出一次油门 - 如果我按下2次按键,它将在10秒完成时触发第二次按键。我不想要这种行为。

21 个答案:

答案 0 :(得分:52)

我会使用underscore.jslodash源代码来查找此功能的经过良好测试的版本。

以下是下划线代码的略微修改版本,用于删除对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;
  };
};

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

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

编辑2:添加了关于lodash和可能的代码简化的建议,对lolzery wowzery的评论

答案 1 :(得分:11)

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

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

时间:重置限额数的时间跨度

功能和用法:假设您有一个允许用户在1分钟内拨打10次的API

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

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

    /// creating a clousre 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 exeeded');
    };
}
    
//////////////////////////////////////////////////////////// 
// how to use

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

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

答案 2 :(得分:7)

这是一个与vsync解决方案相同的限制函数,并添加了将参数传递给限制函数。

function throttle (callback, limit) {

  var wait = false;
  return function () {
    if (!wait) {

      callback.apply(null, arguments);
      wait = true;
      setTimeout(function () {
        wait = false;
      }, limit);
    }
  }
}

可以像这样使用:

window.addEventListener('resize', throttle(function(e){console.log(e)}, 100));

在没有addEventListener上下文的情况下使用:

throttle(function(arg1, arg2){console.log(arg1, arg2);}, 100)('red', 'blue');
// red blue

答案 3 :(得分:4)

添加到此处的讨论(以及更新的访问者),如果不使用throttle中几乎事实上的lodash的原因是要使用较小尺寸的包或捆绑包,那么它&# 39;可能只在您的包中包含throttle而不是整个lodash库。例如在ES6中,它将类似于:

import throttle from 'lodash/throttle';

此外,还有一个名为lodash.throttlethrottle lodash个套餐,可与ES6中的简单import或ES5中的require一起使用。

答案 4 :(得分:3)

我在这里看到很多答案对于“js 中的简单节流阀”来说太复杂了。

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

这里是一个简单的实现,它也处理“限制中”的调用:

{socialLinks.map(social, (index) => (
        <input
          placeholder={social}
          onChange={(e) =>
            db
              .collection("users")
              .doc(user)
              .collection("profile")
              .doc(user)
              .collection("socialMediaLinks")
              .update({
                socialLink: e.target.value,
                socialType: social,
              })
          }
        />
      ))}

对于简单的去抖动,这几乎是完全相同的实现。它只是添加了超时延迟的计算,这需要跟踪函数上次运行的时间。见下文:

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

答案 5 :(得分:2)

这里是我如何在9LOC中实现ES6中的油门功能,希望有所帮助

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

点击此link查看其工作原理。

答案 6 :(得分:1)

这是我自己的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并不是一个好主意。

答案 7 :(得分:1)

那呢?

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

简单。

您可能有兴趣看看source

答案 8 :(得分:1)

对于窗口调整大小事件,我只需要一个油门/去抖功能,并且很好奇,我还想知道它们是什么以及它们如何工作。

我已经阅读了多篇关于SO的博客文章和QA,但它们似乎都使这一过程变得过于复杂,建议使用库,或者只是提供说明,而不是简单的普通JS实现。

由于内容丰富,我将不提供描述。所以这是我的实现:

function throttle(callback, delay) {
    var timeoutHandler = null;
    return function () {
        if (timeoutHandler == null) {
            timeoutHandler = setTimeout(function () {
                callback();
                clearInterval(timeoutHandler);
                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();
                clearInterval(timeoutHandler);
                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

答案 9 :(得分:1)

有一个适用于此目的的库,它是Ember的Backburner.js。

https://github.com/BackburnerJS/

您会这样使用。

var backburner = new Backburner(["task"]); //You need a name for your tasks

function saySomething(words) {
  backburner.throttle("task", console.log.bind(console, words)
  }, 1000);
}


function mainTask() {
  "This will be said with a throttle of 1 second per word!".split(' ').map(saySomething);
}

backburner.run(mainTask)

答案 10 :(得分:1)

我制作了一个带有一些限制功能的npm包:

  

npm install function-throttler

throttleAndQueue

返回函数的一个版本,每W毫秒可以调用一次,其中W是等待的。调用你的func比W更频繁地发生每个W ms调用

throttledUpdate

返回函数的一个版本,每W毫秒可以调用一次,其中W是等待的。对于比W更频繁发生的呼叫,最后一次呼叫将是被呼叫的呼叫(最后优先)

油门

限制你的函数最多每W毫秒被调用,其中W是等待的。 W的呼叫被取消

答案 11 :(得分:0)

此油门功能建立在ES6上。回调函数接受参数(args),但仍与节流函数一起包装。可以根据您的应用需求自由定制延迟时间。每100毫秒1次用于开发模式,事件“ oninput”仅是经常使用的示例:

const callback = (...args) => {
  console.count('callback throttled with arguments:', args);
};

throttle = (callback, limit) => {
  let timeoutHandler = 'null'

  return (...args) => {
    if (timeoutHandler === 'null') {
      timeoutHandler = setTimeout(() => {            
        callback(...args)
        timeoutHandler = 'null'
      }, limit)
    }
  }
}

window.addEventListener('oninput', throttle(callback, 100));

P.S。正如@Anshul解释的那样:随着时间的流逝,节流强制执行一个函数的最大次数。就像“每100毫秒最多执行一次此功能”。

答案 12 :(得分:0)

在下面的示例中,尝试多次单击按钮,但是myFunc函数将仅在3秒钟内执行一次。 函数throttle与要执行的函数和延迟一起传递。它返回一个闭包,该闭包存储在obj.throttleFunc中。 现在,由于obj.throttleFunc存储了闭包,因此isRunning的值将保留在其中。

function throttle(func, delay) {
  let isRunning;
  return function(...args) {
    let context = this;        // store the context of the object that owns this function
    if(!isRunning) {
      isRunning = true;
      func.apply(context,args) // execute the function with the context of the object that owns it
      setTimeout(function() {
        isRunning = false;
      }, delay);
    }
  }
}

function myFunc(param) {
  console.log(`Called ${this.name} at ${param}th second`);
}

let obj = {
  name: "THROTTLED FUNCTION ",
  throttleFunc: throttle(myFunc, 3000)
}

function handleClick() {
  obj.throttleFunc(new Date().getSeconds());
}
button {
  width: 100px;
  height: 50px;
  font-size: 20px;
}
    <button onclick="handleClick()">Click me</button>


  

如果我们不希望传递上下文或参数,则更简单   版本如下:

function throttle(func, delay) {
  let isRunning;
  return function() {
    if(!isRunning) {
      isRunning = true;
      func()
      setTimeout(function() {
        isRunning = false;
      }, delay);
    }
  }
}

function myFunc() {
  console.log('Called');
}


let throttleFunc = throttle(myFunc, 3000);

function handleClick() {
  throttleFunc();
}
button {
  width: 100px;
  height: 50px;
  font-size: 20px;
}
<button onclick="handleClick()">Click me</button>

答案 13 :(得分:0)

我还想提出一个简单的解决方案,以解决只有一个您知道要调用的函数的情况(例如:搜索)

这是我在项目中所做的

let throttle;

function search() {
    if (throttle) {
      clearTimeout(throttle);
    }
    throttle = setTimeout(() => {
      sendSearchReq(str)
    }, 500);
  }

在输入更改事件上调用搜索

答案 14 :(得分:0)

function throttle(targetFunc, delay){
  let lastFunc;
  let lastTime;

  return function(){
    const _this = this;
    const args = arguments;

    if(!lastTime){
      targetFunc.apply(_this, args);
      lastTime = Date.now();
    } else {
      clearTimeout(lastFunc);
      lastFunc = setTimeout(function(){
        targetFunc.apply(_this, args);
        lastTime = Date.now();
      }, delay - (Date.now() - lastTime));
    }
  }
}

尝试一下:

window.addEventListener('resize', throttle(function() {
  console.log('resize!!');
}, 200));

答案 15 :(得分:0)

简单的油门功能-

注意-继续单击该按钮,您将在第一次单击时看到控制台日志,然后每隔5秒钟才看到控制台日志,直到您继续单击为止。

HTML-

<button id='myid'>Click me</button>

JavaScript-

const throttle = (fn, delay) => {
  let lastTime = 0;
  return (...args) => {
      const currentTime = new Date().getTime();
      if((currentTime - lastTime) < delay) {
        return;
      };
      lastTime = currentTime;
      return fn(...args);
  }
};

document.getElementById('myid').addEventListener('click', throttle((e) => {
  console.log('I am clicked');
}, 5000));

答案 16 :(得分:0)

我们还可以使用标志来实现-

var expensive = function(){
    console.log("expensive functionnns");
}

window.addEventListener("resize", throttle(expensive, 500))

function throttle(expensiveFun, limit){
    let flag = true;
    return function(){
        let context = this;
        let args = arguments;
        if(flag){
            expensiveFun.apply(context, args);
            flag = false;
            setTimeout(function(){
                flag = true;
            }, limit);
        }
    }
}

答案 17 :(得分:0)

下面是我能想到的最简单的油门,13 LOC。每次调用函数时都会创建超时并取消旧函数。正如预期的那样,使用适当的上下文和参数调用原始函数。

function throttle(fn, delay) {
  var timeout = null;

  return function throttledFn() {
    window.clearTimeout(timeout);
    var ctx = this;
    var args = Array.prototype.slice.call(arguments);

    timeout = window.setTimeout(function callThrottledFn() {
      fn.apply(ctx, args);
    }, delay);
  }
}

// try it out!
window.addEventListener('resize', throttle(function() {
  console.log('resize!!');
}, 200));

答案 18 :(得分:0)

这是 @clément-prévost answer

的现代化和简化版本

function throttle(func, wait, options = {}) {
  let timeout = null;
  let previous = 0;

  const later = (...args) => {
    previous = options.leading === false ? 0 : Date.now();
    func(...args);
  };

  return (...args) => {
    const now = Date.now();

    if (!previous && options.leading === false) {
      previous = now;
    }

    const remaining = wait - (now - previous);

    if (remaining <= 0 || remaining > wait) {
      if (timeout) {
        clearTimeout(timeout);
        timeout = null;
      }
      previous = now;
      func(...args);
    } else if (options.trailing !== false) {
      clearTimeout(timeout);
      timeout = setTimeout(() => later(...args), remaining);
    }
  };
}

function myFunc(a) {
  console.log(`Log: ${a} ${this.val}`);
}

const myFuncThrottled = throttle(myFunc.bind({val: 42}), 1234, {leading: true, trailing: true})

myFuncThrottled(1)
myFuncThrottled(2)
myFuncThrottled(3)

答案 19 :(得分:0)

CodeSandbox

const { now } = Date;

export default function throttle(func, frameDuration) {
  let timeout = null;
  let latest;
  const epoch = now();

  function getDurationToNextFrame() {
    const elapsed = now() - epoch;
    const durationSinceLastFrame = elapsed % frameDuration;
    return frameDuration - durationSinceLastFrame;
  }

  function throttled(...args) {
    latest = () => {
      func.apply(this, args);
    };
    if (!timeout) {
      timeout = setTimeout(() => {
        latest();
        timeout = null;
      }, getDurationToNextFrame());
    }
  }

  return throttled;
}

答案 20 :(得分:-1)

对于合适的油门功能,人们不需要大量的局部变量。节流功能的目的是减少浏览器资源,而不是应用你正在使用的更多开销。作为这种说法的证据的证据,我设计了一种只有4&#34;悬挂&#34;变量在其范围内。 (A&#34;悬挂&#34;变量是一个永远不会被垃圾收集的变量,因为它总是被一个可以潜在调用的函数引用,从而吸收内存。)少数油门功能通常不会造成任何伤害;但是,如果有数千个节流函数,那么如果使用非常低效的油门功能,内存将开始变得稀缺。我的解决方案如下。

var timenow = self.performance?performance.now.bind(performance):Date.now;
function throttle(func, alternateFunc, minInterval) {
    var lastTimeWent = -1;
    return function() {
        var newTimeWent = timenow();
        if ((newTimeWent-lastTimeWent) > minInterval) {
            lastTimeWent = newTimeWent;
            return func.apply(this, arguments);
        } else if (typeof alternateFunc === "function")
            return alternateFunc.apply(this, arguments);
    };
}

然后,将此限制函数包装在EventTarget中,用于DOM点击,窗口事件,XMLHttpRequests onprogress,FileReader onprogress,[等]等内容,如下所示:

var tfCache = []; // throttled functions cache
function listen(source, eventName, func, _opts){
    var i = 0, Len = tfCache.length, cF = null, options = _opts || {};
    a: {
        for (; i < Len; i += 4)
            if (tfCache[i] === func &&
              tfCache[i+1] === (options.ALTERNATE||null) &&
              tfCache[i+2] === (options.INTERVAL||200)
            ) break a;
        cF = throttle(func, options.ALTERNATE||null, options.INTERVAL||200);
        tfCache.push(func, options.ALTERNATE||null, options.INTERVAL||200, cF);
    }
    source.addEventListener(eventName, cF || tfCache[i+3], _opts);
    return cF === null; // return whether it used the cache or not
};
function mute(source, eventName, func, _opts){
    var options = _opts || {};
    for (var i = 0, Len = tfCache.length; i < Len; i += 4)
        if (tfCache[i] === func &&
          tfCache[i+1] === (options.ALTERNATE||null) &&
          tfCache[i+2] === (options.INTERVAL||200)
        ) {
            source.removeEventListener(eventName, tfCache[i+3], options);
            return true;
        }
    return false;
}

使用示例:

&#13;
&#13;
(function(){"use strict";
// The function throttler //
var timenow = self.performance?performance.now.bind(performance):Date.now;
function throttle(func, alternateFunc, minInterval) {
    var lastTimeWent = -1;
    return function() {
        var newTimeWent = timenow();
        if ((newTimeWent-lastTimeWent) > minInterval) {
            lastTimeWent = newTimeWent;
            return func.apply(this, arguments);
        } else if (typeof alternateFunc === "function")
            return alternateFunc.apply(this, arguments);
    };
}
// The EventTarget wrapper: //
var tfCache = []; // throttled functions cache
function listen(source, eventName, func, _opts){
    var i = 0, Len = tfCache.length, cF = null, options = _opts || {};
    a: {
        for (; i < Len; i += 4)
            if (tfCache[i] === func &&
              tfCache[i+1] === (options.ALTERNATE||null) &&
              tfCache[i+2] === (options.INTERVAL||200)
            ) break a;
        cF = throttle(func, options.ALTERNATE||null, options.INTERVAL||200);
        tfCache.push(func, options.ALTERNATE||null, options.INTERVAL||200, cF);
    }
    source.addEventListener(eventName, cF || tfCache[i+3], _opts);
    return cF === null; // return whether it used the cache or not
};
function mute(source, eventName, func, _opts){
    var options = _opts || {};
    for (var i = 0, Len = tfCache.length; i < Len; i += 4)
        if (tfCache[i] === func &&
          tfCache[i+1] === (options.ALTERNATE||null) &&
          tfCache[i+2] === (options.INTERVAL||200)
        ) {
            source.removeEventListener(eventName, tfCache[i+3], options);
            return true;
        }
    return false;
}
// Finally, the key logger: //
var keysEle = document.getElementById("keyspressed");
var recordKeyStroke = function(dir,color){return function listener(Evt){
    if (Evt.key=="e") { mute(document, 'keydown', listener, downOptions);
                        mute(document, 'keyup', listener, upOptions); }
    if (!Evt.repeat) keysEle.insertAdjacentHTML(
        "afterbegin",
        '<div class="'+(Evt.key=="e"?"red":color)+'">'+dir+Evt.key+'</div>'
    );
}};
var downOptions = {passive:1, ALTERNATE: recordKeyStroke("+","grey") };
listen(document, 'keydown', recordKeyStroke("+","green"), downOptions);
var upOptions = {passive:1, ALTERNATE: recordKeyStroke("-","grey") };
listen(document, 'keyup',   recordKeyStroke("-","green"), upOptions);
})();
&#13;
The keys you press and release are shown below.
Those in grey are ones which were passed up by the throttle.
Those in green are ones that were recorded by the throttle.
When the "e" key is pressed, it is red and keystrokes are no longer recorded.
"+[key]" = keydown and "-[key]" = keyup.
<div id="keyspressed" style="white-space:pre-wrap;font-family:'Roboto Mono',monospace;"></div>
<style>.red{color:#f77}.green{color:#5f5}.grey{color:#aaa}</style>
&#13;
&#13;
&#13;

默认情况下,此功能限制为每200ms最多一次调用。要将间隔更改为不同的毫秒数,请在options参数中传递optionsObject.INTERVAL选项,并将其设置为执行之间所需的最小毫秒数。 (因为定时器并不总是最精确的,)如果你有一个确切的最小间隔,那么我建议你从所需的optionsObject.INTERVAL中减去一两个,以确保它总是至少应该被执行。如果在受限制的函数执行被延迟(由于过多的调用)时需要对限制的func的参数执行某些操作,则使用optionsObject.ALTERNATE选项。这&#34;替代&#34;是一个函数,只要删除对主函数的调用,就会立即调用它来代替主函数。例如,如果您在EventTarget上使用限制函数,但想要对已删除的事件preventDefault(),则使用{ALTERNATE: function(evt){ evt.preventDefault(); }}作为选项对象。

相关问题