如何解决递归异步承诺?

时间:2015-09-11 14:05:27

标签: javascript asynchronous recursion promise es6-promise

我正在玩承诺,我遇到了异步递归承诺的问题。

场景是运动员开始跑100米,我需要定期检查他们是否已经完成,一旦完成,打印他们的时间。

编辑以澄清

在现实世界中,运动员正在服务器上运行。 startRunning涉及对服务器进行ajax调用。 checkIsFinished还涉及对服务器进行ajax调用。下面的代码试图模仿它。代码中的时间和距离是硬编码的,以尽量使事情变得简单。抱歉不清楚。

结束编辑

我希望能够写下以下内容

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)

其中

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var checkIsFinished = function (athlete) {
  return new Promise(function (resolve, reject) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);

    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(checkIsFinished.bind(null, athlete), 1000);
    }    
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log(e); };

我可以看到第一次checkIsFinished创建的承诺永远不会被解决。如何确保解析该承诺以便调用printTime

而不是

resolve(athlete);

我能做到

Promise.resolve(athlete).then(printTime);

但我想尽可能避免这种情况,我真的希望能够写出

startRunning()
  .then(checkIsFinished)
  .then(printTime)
  .catch(handleError)

3 个答案:

答案 0 :(得分:7)

错误在于您传递的函数会向setTimeout返回一个承诺。这个承诺在以太中消失了。创可贴修复可能是在执行函数上进行的:

var checkIsFinished = function (athlete) {
  return new Promise(function executor(resolve) {
    if (athlete.distanceTravelled >= 100) {
      clearInterval(intervalID);
      console.log("finished");
      resolve(athlete);
    } else {
      console.log("not finished yet, check again in a bit");
      setTimeout(executor.bind(null, resolve), 1000);
    }    
  });
};

但是,好吧。我认为这是一个很好的例子,说明为什么人们应该避免promise-constructor anti-pattern(因为混合承诺代码和非承诺代码不可避免地导致这样的错误。)

我遵循的最佳做法是避免此类错误:

  1. 只处理返回promises的异步函数。
  2. 当一个人没有返回一个promise时,用promise构造函数包装它。
  3. 尽可能狭窄地包裹(尽可能少的代码)。
  4. 不要将promise构造函数用于其他任何内容。
  5. 在此之后,我发现代码更易于推理并且更难以进行操作,因为所有内容都遵循相同的模式。

    将此应用到您的示例中让我在这里(为了简洁,我使用的是es6箭头功能。它们适用于Firefox和Chrome 45):

    var console = { log: msg => div.innerHTML += msg + "<br>",
                    error: e => console.log(e +", "+ e.lineNumber) };
    
    var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    var startRunning = () => {
      var athlete = {
        timeTaken: 0,
        distanceTravelled: 0,
        intervalID: setInterval(() => {
          athlete.distanceTravelled += 25;
          athlete.timeTaken += 2.5;
          console.log("updated athlete ");
        }, 2500)
      };
      return wait(2000).then(() => athlete);
    };
    
    var checkIsFinished = athlete => {
      if (athlete.distanceTravelled < 100) {
        console.log("not finished yet, check again in a bit");
        return wait(1000).then(() => checkIsFinished(athlete));
      }
      clearInterval(athlete.intervalID);
      console.log("finished");
      return athlete;
    };
    
    startRunning()
      .then(checkIsFinished)
      .then(athlete => console.log('printing time: ' + athlete.timeTaken))
      .catch(console.error);
    <div id="div"></div>

    请注意checkIsFinished会返回运动员或承诺。这很好,因为.then函数会自动提升您传递给promises的函数的返回值。如果您在其他情况下调用checkIsFinished,则可能需要使用return Promise.resolve(athlete);代替return athlete;自行进行宣传。

    根据Amit的评论进行编辑

    对于非递归答案,请使用此帮助程序替换整个checkIsFinished函数:

    var waitUntil = (func, ms) => new Promise((resolve, reject) => {
      var interval = setInterval(() => {
        try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
      }, ms);
    });
    

    然后执行此操作:

    var athlete;
    startRunning()
      .then(result => (athlete = result))
      .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
      .then(() => {
        console.log('finished. printing time: ' + athlete.timeTaken);
        clearInterval(athlete.intervalID);
      })
      .catch(console.error);
    

    var console = { log: msg => div.innerHTML += msg + "<br>",
                    error: e => console.log(e +", "+ e.lineNumber) };
    
    var wait = ms => new Promise(resolve => setTimeout(resolve, ms));
    
    var waitUntil = (func, ms) => new Promise((resolve, reject) => {
      var interval = setInterval(() => {
        try { func() && resolve(clearInterval(interval)); } catch (e) { reject(e); }
      }, ms);
    });
    
    var startRunning = () => {
      var athlete = {
        timeTaken: 0,
        distanceTravelled: 0,
        intervalID: setInterval(() => {
          athlete.distanceTravelled += 25;
          athlete.timeTaken += 2.5;
          console.log("updated athlete ");
        }, 2500)
      };
      return wait(2000).then(() => athlete);
    };
    
    var athlete;
    startRunning()
      .then(result => (athlete = result))
      .then(() => waitUntil(() => athlete.distanceTravelled >= 100, 1000))
      .then(() => {
        console.log('finished. printing time: ' + athlete.timeTaken);
        clearInterval(athlete.intervalID);
      })
      .catch(console.error);
    <div id="div"></div>

答案 1 :(得分:1)

使用setTimeout / setInterval是一种不能很好地兑现承诺的风格,并且会让你使用皱眉的承诺反模式。

话虽如此,如果你重建你的功能使它成为一个“等待完成”类型的功能(并相应地命名),你将能够解决你的问题。 waitForFinish函数只调用一次,并返回单个promise(虽然是新的,在startRunning中创建的原始promise之上)。通过setTimeout处理重复是在内部轮询函数中完成的,其中使用正确的try / catch来确保异常传播到promise。

var intervalID;
var startRunning = function () {
  var athlete = {
    timeTaken: 0,
    distanceTravelled: 0
  };
  var updateAthlete = function () {
    athlete.distanceTravelled += 25;
    athlete.timeTaken += 2.5;
    console.log("updated athlete", athlete)
  }

  intervalID = setInterval(updateAthlete, 2500);

  return new Promise(function (resolve, reject) {
    setTimeout(resolve.bind(null, athlete), 2000);
  })
};

var waitForFinish = function (athlete) {
  return new Promise(function(resolve, reject) {
    (function pollFinished() {
      try{
        if (athlete.distanceTravelled >= 100) {
          clearInterval(intervalID);
          console.log("finished");
          resolve(athlete);
        } else {
          if(Date.now()%1000 < 250) { // This is here to show errors are cought
            throw new Error('some error');
          }
          console.log("not finished yet, check again in a bit");
          setTimeout(pollFinished, 1000);
        }
      }
      catch(e) { // When an error is cought, the promise is properly rejected
        // (Athlete will keep running though)
        reject(e);
      }
    })();
  });
};

var printTime = function (athlete) {
  console.log('printing time', athlete.timeTaken);
};

var handleError = function (e) { console.log('Handling error:', e); };

startRunning()
  .then(waitForFinish)
  .then(printTime)
  .catch(handleError);

虽然所有这些代码都正常运行,但在异步环境中永远不会建议轮询解决方案,并且应尽可能避免使用。在您的情况下,由于此示例模拟与服务器的通信,我会考虑使用Web套接字。

答案 2 :(得分:0)

由于您对Promises的使用非常不合适,因此有点难以准确地说出您正在尝试做什么或哪种实现最适合,但这是一个建议。

Promises是一次性状态机。因此,您将在未来返回承诺并且恰好一次,承诺可以被理由拒绝或者使用值来解决。鉴于承诺的设计,我认为有意义的事情可以像这样使用:

startRunning(100).then(printTime, handleError);

您可以使用以下代码实现:

function startRunning(limit) {
    return new Promise(function (resolve, reject) {
        var timeStart = Date.now();
        var athlete = {
            timeTaken: 0,
            distanceTravelled: 0
        };
        function updateAthlete() {
            athlete.distanceTravelled += 25;
            console.log("updated athlete", athlete)
            if (athlete.distanceTravelled >= limit) {
                clearInterval(intervalID);
                athlete.timeTaken = Date.now() - timeStart;
                resolve(athlete);
            }
        }
        var intervalID = setInterval(updateAthlete, 2500);
    });
}

function printTime(athlete) {
    console.log('printing time', athlete.timeTaken);
}

function handleError(e) { 
    console.log(e); 
}

startRunning(100).then(printTime, handleError);

工作演示:http://jsfiddle.net/jfriend00/fbmbrc8s/

仅供参考,我的设计偏好可能是拥有一个公共运动员对象,然后该对象的方法开始运行,停止运行等...

以下是使用承诺时遇到的一些基本问题:

  1. 它们是一次性物体。它们只被解决或拒绝一次。
  2. 结构startRunning().then(checkIsFinished)只是没有逻辑意义。对于这个工作的第一部分,startRunning()必须返回一个承诺,它必须解决矿石拒绝承诺当有用的事情。你只需要在两秒钟之后解决它,这似乎没有什么用处。
  3. 你的描述中的单词听起来好像你想要`checkIsFinished()继续不解决它的承诺,直到运动员完成。可以通过不断地链接承诺来做到这一点,但这似乎是一种非常复杂的做事方式,在这里肯定没有必要。此外,这完全不是您的代码尝试做的事情。您的代码只返回一个永远不会解决的新承诺,除非athelete已经超过了所需的距离。如果不是,则返回从未解决或拒绝的承诺。这是对承诺概念的根本违反。返回promise的函数负责最终解析或拒绝它,除非调用代码期望放弃承诺,在这种情况下它可能是错误的设计工具。
  4. 这是另一种创建公共Athlete()对象的方法,该对象具有一些方法,允许多人观看进度:

    var EventEmitter = require('events');
    
    function Athlete() {
        // private instance variables
        var runInterval, startTime; 
        var watcher = new EventEmitter();
    
        // public instance variables
        this.timeTaken = 0;
        this.distanceTravelled = 0;
        this.startRunning = function() {
            startTime = Date.now();
            var self = this;
            if (runInterval) {clearInterval(runInterval);}
            runInterval = setInterval(function() {
                self.distanceTravelled += 25;
                self.timeTaken = Date.now() - startTime;
                console.log("distance = ", self.distanceTravelled);
                // notify watchers
                watcher.emit("distanceUpdate");
            },2500);
        }
        this.notify = function(limit) {
            var self = this;
            return new Promise(function(resolve, reject) {
                function update() {
                    if (self.distanceTravelled >= limit) {
                        watcher.removeListener("distanceUpdate", update);
                        resolve(self);
                        // if no more watchers, then stop the running timer
                        if (watcher.listeners("distanceUpdate").length === 0) {
                            clearInterval(runInterval);
                        }
                    }
                }
                watcher.on("distanceUpdate", update);
            });
        }
    }
    
    var a = new Athlete();
    a.startRunning();
    a.notify(100).then(function() {
        console.log("done");
    });