如何延迟函数的执行,JavaScript

时间:2017-03-07 14:55:43

标签: javascript node.js

背景

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

出于这个问题的目的,这将是我所指的异步函数:

/*
 *  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正在使用的资源。

目的

为避免上一个问题,我希望延迟执行Xms对asyncMoc的所有调用。

这是我假装的图片:

delayed_requests

为实现这一目标,我写了以下方法:

  1. 使用承诺
  2. 使用setInterval
  3. 使用承诺

    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

    在这种方法中,我的目标是建立一个具有一系列参数的工厂。然后,每500ms,计时器将运行一个执行器,它将从该数组中获取一个参数并返回一个结果:

    /*
     *  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 3次没有任何延迟,总是使用相同的参数,然后因为我不知道如何完成我的else分支,它什么都不做。

    问题:

    1. 哪种方法更适合实现我的目标以及如何解决?

3 个答案:

答案 0 :(得分:1)

好的,所以这是我解决问题的方法。抱歉,我必须重写您的代码才能更好地理解它。我希望你无论如何都能解释它并从中得到一些东西。

使用Promises(JSFiddle):

在彼此之间调用500毫秒
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"}

答案 1 :(得分: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(由于计时器变幻而给予或花费一毫秒),并确保第一个调用延迟至少{{ 1}}。

带有一些调试信息的实例:

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);

答案 2 :(得分: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提案的巨大改进,原因如下:

  • falsiness。 Falsy值和表达式(如!executing)是JavaScript中的一个问题。我强烈推荐JavaScript Appendix A: Awful parts
  • 如果catch失败
  • ,则实施asyncMock
  • 使用Array.prototype.shift代替Array.prototype.splice,更容易阅读并提高效果。
  • 不使用new关键字,不要弄乱this引用
  • 没有内在的功能。 ESlint会感谢你:P
  • 使用factories Douglas Crockford style

如果你喜欢Arg0n的解决方案,我建议你看看我的。

@ Arg0n VS @ T.J。克劳德......战斗!

哪种解决方案更好,为什么?

起初我倾向于Arg0n的解决方案,因为它从我失败的尝试中获取灵感并使其成功。就其本身而言,这是非凡的。

此外,JavaScript中的Timers存在精度问题,并且在使用数字进行计算时JavaScript也存在问题(检查0.1 + 0.2!== 0.3)。

但是,两种解决方案都使用Timers。实际上,您需要计时器才能实现此行为。此外@ T.J。 Crowder的解决方案不是使用浮点算术,而是整数,因此他的计算是安全和合理的。

有人可能会指出Math库是从java导入的JavaScript中的一个错误,但老实说这很远,而且没有任何问题。

此外,由于T.J。的解决方案没有像Arg0n的解决方案那样的数据结构,因此它的代码较小,因为它包含较少的逻辑来维护。从技术角度来看,毫无疑问,在这种具体情况下,他的解决方案是可行的。

然而,对于那些不熟悉数学的人来说,Arg0n的大道非常扎实。

结论

从技术角度来看,T.J。的解决方案获胜。不过我可以说我很喜欢Arg0n的解决方案,特别是我的帖子版本,这是我可能会使用的版本。

我希望这篇文章可以帮助将来的某个人!