繁体   English   中英

如何解决递归异步承诺?

[英]How to resolve recursive asynchronous promises?

我正在玩promise,但遇到异步递归promise的麻烦。

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

编辑以澄清

在现实世界中,运动员正在服务器上运行。 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调用printTime

代替

resolve(athlete);

我可以做

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

但我想避免这种情况,我真的很想能够写

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

错误是您传递了一个函数,该函数返回一个对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反模式 (因为混合使用promise代码和非Promise代码不可避免地会导致此类错误)。

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

  1. 仅处理返回承诺的异步函数。
  2. 当一个人不返回诺言时,用诺言构造函数包装它。
  3. 尽可能地使其包装(使用最少的代码)。
  4. 不要将promise构造函数用于其他任何事情。

在此之后,我发现代码更容易推理,更难以调试,因为所有内容都遵循相同的模式。

将其应用于您的示例使我到了这里(为了简便起见,我使用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函数会自动提升您传递给promise的函数的返回值。 如果要在其他情况下调用checkIsFinished ,则可能需要使用return Promise.resolve(athlete);自己进行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> 

使用setTimeout / setInterval是不能很好地兑现诺言的场景之一,并且会导致您使用皱眉的诺言反模式。

话虽如此,如果您将函数重构为“等待完成”类型的函数(并相应地命名),则可以解决您的问题。 waitForFinish函数仅被调用一次,并返回一个promise(尽管在startRunning创建的原始promise的基础上是一个新startRunning )。 通过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套接字。

由于您对诺言的使用还差得很远,因此很难确切说明您要做什么或最适合哪种实现,但这是一个建议。

承诺是一站式状态机。 这样,您将返回一个承诺,并且恰好在将来的某个时间返回该承诺,或者有理由拒绝该承诺,也可以通过值来解决该承诺。 鉴于承诺的设计,我认为有意义的是可以这样使用:

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()startRunning()必须返回一个promise,并且它必须在发生有用的事情时解析或拒绝该promise。 您只是在两秒钟后解决了问题,但这似乎并没有完成任何有用的工作。
  3. 您所描述的文字听起来像是您希望`checkIsFinished()继续前进,直到运动员完成才解决其承诺。 可以通过连续地链接诺言来做到这一点,但这似乎是做事的非常复杂的方式,在这里当然不是必需的。 此外,这根本不是您的代码尝试执行的操作。 您的代码只会返回一个新的承诺,除非运动员已经通过了期望的距离,否则它将永远无法解决。 如果不是,它将返回从未解决或拒绝的承诺。 这是对诺言概念的根本违反。 返回承诺的函数负责最终解决或拒绝承诺,除非调用代码期望放弃承诺,在这种情况下,这可能是错误的设计工具。

这是创建公共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");
});

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM