如何延迟执行函数,JavaScript

3

背景

我正在尝试创建一个工厂函数,该函数可以在给定延迟后执行特定的异步函数。

为了解决这个问题,以下是我所指的异步函数:

/*
 *  This is a simulation of an async function. Be imaginative! 
 */
let asyncMock = function(url) {
    return new Promise(fulfil => {

        setTimeout(() => {
            fulfil({
                url,
                data: "banana"
            });
        }, 10000);

    });
};

这个函数接收一个url,并返回一个包含该URL和一些数据的JSON对象。
在我的代码中,我以以下方式调用这个函数:
asyncMock('http://www.bananas.pt')
.then(console.log);

asyncMock('http://www.berries.com')
.then(console.log);

//... badjillion more calls

asyncMock('http://www.oranges.es')
.then(console.log);

问题

这里的问题是所有这些调用都在完全相同的时间内进行,从而过载了asyncMoc正在使用的资源。

目标

为了避免上述问题,我希望延迟对asyncMoc的所有调用X毫秒。

以下是我想要的图形:

delayed_requests

为了实现这一点,我编写了以下方法:

  1. 使用Promises
  2. 使用setInterval

使用Promises

let asyncMock = function(url) {
  return new Promise(fulfil => {

    setTimeout(() => {
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};

let delayFactory = function(args) {

  let {
    delayMs
  } = args;

  let promise = Promise.resolve();

  let delayAsync = function(url) {
    return promise = promise.then(() => {

      return new Promise(fulfil => {
        setTimeout(() => {
          console.log(`made request to ${url}`);
          fulfil(asyncMock(url));
        }, delayMs);
      });
    });
  };

  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.veggies.com')
  .then(console.log)
  .catch(console.error);

这个工厂有一个名为delayAsync的函数,可以使所有对asyncMock的调用延迟500毫秒。 然而,它也强制执行调用的嵌套等待前一个调用的结果,这是不打算的。
目标是在500毫秒内分别进行三次对asyncMock的调用,并在10秒后获得具有500毫秒差异的三个响应。

使用setInterval

在这种方法中,我的目标是拥有一个包含参数数组的工厂。然后,每隔500毫秒,计时器将运行一个执行程序,该执行程序将从该数组中取出一个参数并返回一个带有该参数的结果:

/*
 *  This is a simulation of an async function. Be imaginative! 
 */
let asyncMock = function(url) {
  return new Promise(fulfil => {

    setTimeout(() => {
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};


let delayFactory = function(args) {

  let {
    throttleMs
  } = args;

  let argsList = [];
  let timer;

  /*
   *  Every time this function is called, I add the url argument to a list of 
   *  arguments. Then when the time comes, I take out the oldest argument and 
   *  I run the mockGet function with it, effectively making a queue.
   */
  let delayAsync = function(url) {
    argsList.push(url);

    return new Promise(fulfil => {

      if (timer === undefined) {

        console.log('created timer');
        timer = setInterval(() => {

          if (argsList.length === 0) {
            clearInterval(timer);
            timer = undefined;
          } else {
            let arg = argsList.shift();

            console.log('making  request ' + url);
            fulfil(asyncMock(arg));
          }
        }, throttleMs);

      } else {
        //what if the timer is already running? I need to somehow 
        //connect it to this call!
      }
    });
  };



  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.veggies.com')
  .then(console.log)
  .catch(console.error);
// a ton of other calls in random places in code

这段代码更糟糕。它无任何延迟地执行了三次 asyncMoch,总是使用相同的参数,然后由于我不知道如何完成我的 else 分支,所以什么也没做。

问题:

  1. 哪种方法更好来达到我的目标,如何修复它?

1
我不太明白你的目标,请你能详细解释一下吗?(或许可以参考我在这个评论中提到的问题,链接在这里:https://dev59.com/Mp_ha4cB1Zd3GeqP2qxL#80QcoYgBc1ULPQZFRrjI?) - T.J. Crowder
你的意思是说 url 对于所有调用来说始终是上一次函数调用中使用的吗?如果是这样,那么我在你的代码中没有遇到这个问题。 - Arg0n
1
我改进了问题,并添加了一个图像,展示我的意图。希望现在更清楚了,因为我不知道如何更好地解释它 :S - Flame_Phoenix
你还没有确认/拒绝我在我的上一个评论中的第二项:throttleAsync的Promise(我猜现在是delayAsync)是否需要基于asyncMock的Promise的解决方案来解决? - T.J. Crowder
我不希望第二个调用等待第一个的响应。如果throttleAsync可以根据它接收到的请求来解决,那就太棒了。但我并没有看到实现这一点的方法。 - Flame_Phoenix
@Flame_Phoenix:只需要删除一个Promise和下一个之间的链接即可;我已经将其发布为答案。 :-) - T.J. Crowder
3个回答

1

介绍

阅读了两个解决方案后,我必须感谢那些花时间帮助我的人。正是这样的时刻(虽然很少),让我为拥有 StackOverflow 账户而感到自豪。

话虽如此,在阅读了两种提案之后,我提出了自己的一个方案,并将解释我认为哪个方案最好以及原因。

我的解决方案

我的解决方案基于 @Arg0n 的提案,是他在 JavaScript 中使用工厂模式进行简化/重新实现的代码,采用了 Douglas Crockford 提出的 ECMA6 特性:

let asyncFunc = function(url) {
  return new Promise((resolve, reject) => {
    setTimeout(function() {
      resolve({
        url: url,
        data: 'banana'
      });
    }, 5000);
  });
};

let delayFactory = function(args) {
  let {
    delayMs
  } = args;
  let queuedCalls = [];
  let executing = false;

  let queueCall = function(url) {
    return new Promise((resolve, reject) => {

      queuedCalls.push({
        url,
        resolve,
        reject
      });

      if (executing === false) {
        executing = true;
        nextCall();
      }
    });
  };

  let execute = function(call) {

    console.log(`sending request ${call.url}`);

    asyncFunc(call.url)
      .then(call.resolve)
      .catch(call.reject);

    setTimeout(nextCall, delayMs);
  };

  let nextCall = function() {
    if (queuedCalls.length > 0)
      execute(queuedCalls.shift());
    else
      executing = false;
  };

  return Object.freeze({
    queueCall
  });
};

let myFactory = delayFactory({
  delayMs: 1000
});

myFactory.queueCall('http://test1')
  .then(console.log)
  .catch(console.log);

myFactory.queueCall('http://test2')
  .then(console.log)
  .catch(console.log);

myFactory.queueCall('http://test3')
  .then(console.log)
  .catch(console.log);

为什么我要发布这个额外的解决方案?因为我认为它比Arg0n的提案有很大改进,原因如下:

  • 没有假值。在JavaScript中,假值和表达式(如!executing)是一个问题。我强烈推荐阅读附录A:糟糕的部分
  • 如果asyncMock失败,则实现catch
  • 使用Array.prototype.shift而不是Array.prototype.splice,这样更易于阅读并提高性能。
  • 没有使用new关键字,没有破坏this引用
  • 没有内部函数。ESlint会感谢你 :P
  • 使用Douglas Crockford风格的工厂
如果您喜欢Arg0n的解决方案,我建议您看看我的解决方案。
@Arg0n VS @T.J. Crowder ... FIGHT!
哪个解决方案更好,为什么?
起初,我倾向于Arg0n的解决方案,因为它从我的一个失败尝试中汲取灵感并使其起作用。这本身就是值得注意的。
此外,JavaScript中的计时器存在精度问题,并且在使用数字进行计算时也存在问题(请检查0.1 + 0.2!= 0.3)。
但是,两种解决方案都使用计时器。实际上,您需要计时器才能实现此行为。此外,@T.J. Crowder的解决方案不使用浮点数进行算术运算,而是使用整数,因此他的计算是安全可靠的。
有人可能会指出,在JavaScript中,Math库是从java中导入的错误,但老实说,这太过分了,没有任何问题。
此外,由于T.J.的解决方案没有像Arg0n的解决方案那样具有数据结构,因此其代码较小,包含的逻辑较少。从技术角度来看,在这种特定情况下,他的解决方案是应该选择的。

然而,对于那些不精通其中数学知识的人来说,Arg0n的方法是一个相当可靠的选择。

结论

从技术角度来看,T.J.的解决方案获胜。但我可以说,我非常喜欢Arg0n的解决方案,尤其是他帖子中的版本,这也是我可能会使用的版本。

希望这篇文章能够在未来帮助到某些人!


干得好!虽然我的代码并不完美,但我只用了大约20分钟的时间来让你朝着正确的方向前进 :) - Arg0n

1

好的,这是我对你的问题的解决方案。抱歉我不得不重写你的代码以便更好地理解它。我希望你能理解并从中获得一些东西。

使用Promises之间相互调用500ms(JSFiddle):

function asyncFunc(url) {
    return new Promise(resolve => {
    setTimeout(function() {
        resolve({ url: url, data: 'banana' });
    }, 2000);
  });
}

function delayFactory(delayMs) {
  var delayMs = delayMs;
  var queuedCalls = [];
  var executing = false;

  this.queueCall = function(url) {
    var promise = new Promise(function(resolve) {
        queuedCalls.push({ url: url, resolve: resolve });
        executeCalls();
    });
    return promise;
  }

  var executeCalls = function() {
    if(!executing) {
      executing = true;
      function execute(call) {
        asyncFunc(call.url).then(function(result) {
            call.resolve(result);
        });
        setTimeout(function() {
            queuedCalls.splice(queuedCalls.indexOf(call), 1);
          if(queuedCalls.length > 0) {
            execute(queuedCalls[0]);
          } else {
            executing = false;
          }
        }, delayMs)
      }
      if(queuedCalls.length > 0) {
        execute(queuedCalls[0]);
      }
    }
  }
}

var factory = new delayFactory(500);
factory.queueCall('http://test1').then(console.log); //2 sec log {url: "http://test1", data: "banana"}
factory.queueCall('http://test2').then(console.log); //2.5 sec log {url: "http://test2", data: "banana"}
factory.queueCall('http://test3').then(console.log); //3 sec log {url: "http://test3", data: "banana"}
factory.queueCall('http://test4').then(console.log); //3.5 sec log {url: "http://test4", data: "banana"}

我不知道是谁给这个投票打了反对票,但我会给它点个赞。即使它不正确,它也表现出了努力。是的,我可以像那样一个接一个地执行每个函数,但如果我按照你的方式去做,调用之间就不会有任何延迟,第二次调用将被迫等待第一次的结果,依此类推,这并不是我们想要的。 - Flame_Phoenix
实际上,在评论中点“踩”几乎总是没有用的。不过,评论答案的错误之处可能会有帮助。在这种情况下:这根本没有回答问题,它没有考虑到在调用之间延迟的表达愿望,也没有涉及承诺和计时器的问题。 - T.J. Crowder
是的,这是为了更简单地回答问题而尝试的,因为我认为问问题的人过于复杂化了事情。 - Arg0n
这非常有趣,我一定会尝试一下! - Flame_Phoenix
你有设置“执行”标志的原因吗?还是只是为了以后可能需要而添加的附加功能? - Flame_Phoenix
显示剩余4条评论

1
我假设您希望由delayAsync返回的承诺基于asyncMock的承诺解析。

如果是这样,我会使用基于承诺的方法并进行修改(请参见注释):

// Seed our "last call at" value
let lastCall = Date.now();
let delayAsync = function(url) {
  return new Promise(fulfil => {
    // Delay by at least `delayMs`, but more if necessary from the last call
    const now = Date.now();
    const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
    lastCall = now + thisDelay;
    setTimeout(() => {
      // Fulfill our promise using the result of `asyncMock`'s promise
      fulfil(asyncMock(url));
    }, thisDelay);
  });
};

这确保每次调用asyncMock至少在前一次之后等待delayMs(由于计时器的不确定性可能会有微秒级别的差异),并确保第一个调用至少延迟delayMs
带有一些调试信息的实时示例:

let lastActualCall = 0; // Debugging only
let asyncMock = function(url) {
  // Start debugging
  // Let's show how long since we were last called
  console.log(Date.now(), "asyncMock called", lastActualCall == 0 ? "(none)" : Date.now() - lastActualCall);
  lastActualCall = Date.now();
  // End debugging
  return new Promise(fulfil => {

    setTimeout(() => {
      console.log(Date.now(), "asyncMock fulfulling");
      fulfil({
        url,
        data: "banana"
      });
    }, 10000);

  });
};

let delayFactory = function(args) {

  let {
    delayMs
  } = args;

  // Seed our "last call at" value
  let lastCall = Date.now();
  let delayAsync = function(url) {
    // Our new promise
    return new Promise(fulfil => {
      // Delay by at least `delayMs`, but more if necessary from the last call
      const now = Date.now();
      const thisDelay = Math.max(delayMs, lastCall - now + 1 + delayMs);
      lastCall = now + thisDelay;
      console.log(Date.now(), "scheduling w/delay =", thisDelay);
      setTimeout(() => {
        // Fulfill our promise using the result of `asyncMock`'s promise
        fulfil(asyncMock(url));
      }, thisDelay);
    });
  };

  return Object.freeze({
    delayAsync
  });
};

/*
 *  All calls to any of its functions will have a separation of X ms, and will
 *  all be executed in the order they were called. 
 */
let delayer = delayFactory({
  delayMs: 500
});

console.log('running');

delayer.delayAsync('http://www.bananas.pt')
  .then(console.log)
  .catch(console.error);

delayer.delayAsync('http://www.fruits.es')
  .then(console.log)
  .catch(console.error);

// Let's hold off for 100ms to ensure we get the spacing right
setTimeout(() => {
  delayer.delayAsync('http://www.veggies.com')
    .then(console.log)
    .catch(console.error);
}, 100);
.as-console-wrapper {
  max-height: 100% !important;
}


数学方法。我的第一个尝试彻底失败了。你能解释一下你正在做的数学吗?谢谢你的回答!(正是我在寻找的!) - Flame_Phoenix
@Flame_Phoenix:由于我们想要延迟从最后一次调用时间(可以在未来)从现在开始(以后的时间),lastCall - now + 1 告诉我们 lastCall 距离未来(正数)或过去(负数)多少毫秒。然后我们加上 delayMs。如果 lastCall 在过去,那么这个数字将小于 delayMs;但是如果 lastCall 在未来,它可能大于 delayMs。因此,我们取两个值中较大的一个作为延迟时间。然后我们记住下一次调度的时间。(+1 可能有点多余。) - T.J. Crowder
嗨,T.J. Crowder,我相信您可以通过删除表达式“Math.max(delayMs, lastCall - now + 1 + delayMs)”来改进此代码,因为“delayMs” < “lastCall - now + delayMs”。 - Soldeplata Saketos
@SoldeplataSaketos:不,第一次调用很可能会有delayMs > lastCall - now + 1 + delayMs,如果调用之间的间隔足够长,这也可能是真的。 - T.J. Crowder
哦,没错,我没有考虑到大间隔!!我想'+1'在数学上可能有某些原因,但由于JavaScript的本质不是那么精确的时间,所以我想它并不需要。 - Soldeplata Saketos
@SoldeplataSaketos:实际上,+1很可能是一个hack。我偶尔会遇到真正分离的499ms,所以我决定加一个调整。 - T.J. Crowder

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