任务队列 - 限制 JavaScript 函数调用,但使用队列(不要丢弃调用)



函数如何限制其调用速率?如果调用过于频繁,则不应丢弃这些调用,而是应排队并按时间间隔(间隔X毫秒)。我研究过throught和debounce,但它们会丢弃呼叫,而不是排队等待将来运行。

有比以X毫秒为间隔设置process()方法的队列更好的解决方案吗?JS框架中有这样的标准实现吗?到目前为止,我已经看了undercore.js,什么都没有。

如果没有库,应该相当简单:

var stack = [], 
    timer = null;
function process() {
    var item = stack.shift();
    // process
    if (stack.length === 0) {
        clearInterval(timer);
        timer = null;
    }
}
function queue(item) {
    stack.push(item);
    if (timer === null) {
        timer = setInterval(process, 500);
    }
}

http://jsfiddle.net/6TPed/4/

下面是一个示例,它将this(或允许您设置自定义的)

function RateLimit(fn, delay, context) {
    var canInvoke = true,
        queue = [],
        timeout,
        limited = function () {
            queue.push({
                context: context || this,
                arguments: Array.prototype.slice.call(arguments)
            });
            if (canInvoke) {
                canInvoke = false;
                timeEnd();
            }
        };
    function run(context, args) {
        fn.apply(context, args);
    }
    function timeEnd() {
        var e;
        if (queue.length) {
            e = queue.splice(0, 1)[0];
            run(e.context, e.arguments);
            timeout = window.setTimeout(timeEnd, delay);
        } else
            canInvoke = true;
    }
    limited.reset = function () {
        window.clearTimeout(timeout);
        queue = [];
        canInvoke = true;
    };
    return limited;
}

现在

function foo(x) {
    console.log('hello world', x);
}
var bar = RateLimit(foo, 1e3);
bar(1); // logs: hello world 1
bar(2);
bar(3);
// undefined, bar is void
// ..
// logged: hello world 2
// ..
// logged: hello world 3

虽然其他人提供的代码片段确实有效(我已经基于它们构建了一个库),但对于那些想要使用支持良好的模块的人来说,以下是最佳选择:

  • 最流行的速率限制器是限制器
  • function-rate-limit有一个简单的API可以工作,并且在npmjs上有很好的使用统计数据
  • valvallet,一个较新的模块,声称通过支持承诺做得更好,但还没有受到欢迎

我需要一个TypeScript版本,所以我用了@Dan Dasselescu的fiddle并添加了类型。

如果你能提高打字能力,请留言🙏

function rateLimit<T>(
  fn: (...args: Array<any>) => void,
  delay: number,
  context?: T
) {
  const queue: Array<{ context: T; arguments: Array<any> }> = []
  let timer: NodeJS.Timeout | null = null
  function processQueue() {
    const item = queue.shift()
    if (item) {
      fn.apply<T, Array<any>, void>(item.context, item.arguments)
    }
    if (queue.length === 0 && timer) {
      clearInterval(timer)
      timer = null
    }
  }
  return function limited(this: T, ...args: Array<any>) {
    queue.push({
      context: context || this,
      arguments: [...args],
    })
    if (!timer) {
      processQueue() // start immediately on the first invocation
      timer = setInterval(processQueue, delay)
    }
  }
}

我认为这可以变得更简单,并且可以与装饰器一起使用:

/**
 * Sleeps async for a given amount of time.
 * @param milisec
 * @returns
 */
function asyncDelay(milisec: number): Promise<void> {
  return new Promise<void>((resolve) => {
    setTimeout(() => { resolve(); }, milisec);
  });
}
/**
 * Generates a random int within the max and min range.
 * Maximum is exclusive and minimum is inclusive.
 * @param min
 * @param max
 */
export const getRandomInt = (
  min: number,
  max: number,
): number => (
  Math.floor(
    Math.random() * (
      Math.floor(max) - Math.ceil(min)
    ) + Math.ceil(min),
  )
);
/**
 * Will throttle a method by a fixed ms time, if number is passed.
 * if tuple, will throttle by a random time (ms) between each.
 * @param milliseconds
 * @returns
 */
function throttle(milliseconds: number | [number, number]): any {
  let lastCall = 0;
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    const originalMethod = descriptor.value;
    descriptor.value = async function (...args: any[]) {
      const now = Date.now();
      if (!lastCall) {
        lastCall = now;
        return originalMethod.apply(this, args);
      }
      const ms = Array.isArray(milliseconds)
        ? getRandomInt(milliseconds[0], milliseconds[1])
        : Math.round(milliseconds);
      const diff = now - lastCall;
      if (diff < ms) {
        await asyncDelay(ms - diff);
      }
      lastCall = Date.now();
      return originalMethod.apply(this, args);
    };
    return descriptor;
  };
}

你可以做的:

class MyClass {
  @throttle(1000)
  async fixedRateLimitedMethod() {
    await doSomething()
  }
  @throttle([1000, 5000])
  async randomRateLimitedMethod() {
    await doSomething()
  }
}

TS游乐场

最新更新