简体   繁体   English

如何解决递归异步承诺?

[英]How to resolve recursive asynchronous promises?

I'm playing around with promises and I'm having trouble with an asynchronous recursive promise. 我正在玩promise,但遇到异步递归promise的麻烦。

The scenario is an athlete starts running the 100m, I need to periodically check to see if they have finished and once they have finished, print their time. 场景是一名运动员开始跑100m,我需要定期检查他们是否已经完成,并且一旦完成,就打印他们的时间。

Edit to clarify : 编辑以澄清

In the real world the athlete is running on a server. 在现实世界中,运动员正在服务器上运行。 startRunning involves making an ajax call to the server. startRunning涉及对服务器进行ajax调用。 checkIsFinished also involves making an ajax call to the server. checkIsFinished还涉及对服务器进行ajax调用。 The code below is an attempt to imitate that. 下面的代码是试图模仿它。 The times and distances in the code are hardcoded in an attempt to keep things as simple as possible. 代码中的时间和距离被硬编码,以使事情尽可能简单。 Apologies for not being clearer. 抱歉,不清楚。

End edit 结束编辑

I'd like to be able to write the following 我想写以下内容

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

where 哪里

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

I can see that the promise that is created the first time checkIsFinished is never resolved. 我可以看到,第一次checkIsFinished创建的承诺从未解决过。 How can I ensure that that promise is resolved so that printTime is called? 如何确保已解决诺言, printTime调用printTime

Instead of 代替

resolve(athlete);

I could do 我可以做

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

But I'd like to avoid that if possible, I'd really like to be able to write 但我想避免这种情况,我真的很想能够写

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

The bug is that you are passing a function that returns a promise to setTimeout . 错误是您传递了一个函数,该函数返回一个对setTimeout的承诺。 That promise is lost into the ether. 那个应许丢进了以太。 A band-aid fix might be to recurse on the executor function: 临时解决方案可能是递归执行程序函数:

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

But meh. 但是,嗯。 I think this is a great example of why one should avoid the promise-constructor anti-pattern (because mixing promise code and non-promise code inevitably leads to bugs like this). 我认为这是一个很好的示例,说明了为什么应该避免使用promise-constructor反模式 (因为混合使用promise代码和非Promise代码不可避免地会导致此类错误)。

Best practices I follow to avoid such bugs: 为避免此类错误,我遵循的最佳做法:

  1. Only deal with async functions that return promises. 仅处理返回承诺的异步函数。
  2. When one doesn't return a promise, wrap it with a promise constructor. 当一个人不返回诺言时,用诺言构造函数包装它。
  3. Wrap it as narrowly (with as little code) as possible. 尽可能地使其包装(使用最少的代码)。
  4. Don't use the promise constructor for anything else. 不要将promise构造函数用于其他任何事情。

After this, I find code easier to reason about and harder to bugger up, because everything follows the same pattern. 在此之后,我发现代码更容易推理,更难以调试,因为所有内容都遵循相同的模式。

Applying this to your example got me here (I'm using es6 arrow functions for brevity. They work in Firefox and Chrome 45): 将其应用于您的示例使我到了这里(为了简便起见,我使用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> 

Note that checkIsFinished returns either athlete or a promise. 请注意, checkIsFinished返回运动员或诺言。 This is fine here because .then functions automatically promote return values from functions you pass in to promises. 这很好,因为.then函数会自动提升您传递给promise的函数的返回值。 If you'll be calling checkIsFinished in other contexts, you might want to do the promotion yourself, using return Promise.resolve(athlete); 如果要在其他情况下调用checkIsFinished ,则可能需要使用return Promise.resolve(athlete);自己进行return Promise.resolve(athlete); instead of return athlete; 代替return athlete; .

Edit in response to comments from Amit : 根据Amit的评论进行编辑

For a non-recursive answer, replace the entire checkIsFinished function with this helper: 对于非递归答案,请使用以下帮助器替换整个checkIsFinished函数:

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

and then do this: 然后执行以下操作:

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> 

Using setTimeout / setInterval is one of the scenrios that doesn't play well with promises, and causes you to use the frowned promise anti-pattern. 使用setTimeout / setInterval是不能很好地兑现诺言的场景之一,并且会导致您使用皱眉的诺言反模式。

Having said that, if you reconstruct your function make it a "wait for completion" type of function (and name it accordingly as well), you'd be able to solve your problem. 话虽如此,如果您将函数重构为“等待完成”类型的函数(并相应地命名),则可以解决您的问题。 The waitForFinish function is only called once, and returns a single promise (albeit a new one, on top of the original promise created in startRunning ). waitForFinish函数仅被调用一次,并返回一个promise(尽管在startRunning创建的原始promise的基础上是一个新startRunning )。 The handling of the recurrence through setTimeout is done in an internal polling function, where proper try/catch is used to ensure exceptions are propagated to the 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); 

While all this code is functioning properly, a polling solution is never advised in an asynchronous environment and should be avoided if possible. 尽管所有这些代码都可以正常运行,但绝不建议在异步环境中使用轮询解决方案,并且应尽可能避免使用轮询解决方案。 In your case, since this sample mocks communication with a server, I'd consider using web sockets if possible. 在您的情况下,由于此示例模拟了与服务器的通信,因此,如果可能,我会考虑使用Web套接字。

Since your use of promises is pretty off the mark, it's a little hard to tell exactly what you're trying to do or what implementation would best fit, but here's a recommendation. 由于您对诺言的使用还差得很远,因此很难确切说明您要做什么或最适合哪种实现,但这是一个建议。

Promises are a one-shot state machine. 承诺是一站式状态机。 As such, you return a promise and exactly one time in the future, the promise can be either rejected with a reason or resolved with a value. 这样,您将返回一个承诺,并且恰好在将来的某个时间返回该承诺,或者有理由拒绝该承诺,也可以通过值来解决该承诺。 Given that design of promises, I think what makes sense would be something that can be used like this: 鉴于承诺的设计,我认为有意义的是可以这样使用:

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

You could implement that with code like this: 您可以使用以下代码来实现:

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

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


FYI, my design preference would probably be to have a public athlete object and then methods on that object to start running, stop running, etc... 仅供参考,我的设计偏好可能是拥有一个公共运动员对象,然后对该对象执行开始运行,停止运行等方法。


Here are some of the fundamental things you got wrong in a use of promises: 以下是在使用诺言时犯错的一些基本问题:

  1. They are one-shot objects. 它们是一次性的对象。 They are resolved or rejected only once. 它们仅被解决或拒绝一次。
  2. The structure startRunning().then(checkIsFinished) just doesn't make logical sense. 结构startRunning().then(checkIsFinished)只是不合逻辑。 For the first part of this to work, startRunning() has to return a promise, and it has to resolve ore reject that promise when something useful happens. 为了使此工作的第一部分startRunning()startRunning()必须返回一个promise,并且它必须在发生有用的事情时解析或拒绝该promise。 Your are just resolving it after two seconds which doesn't really seem to accomplish anything useful. 您只是在两秒钟后解决了问题,但这似乎并没有完成任何有用的工作。
  3. The words of your description make it sound like you want `checkIsFinished() to keep going an not resolve its promise until the athlete is finished. 您所描述的文字听起来像是您希望`checkIsFinished()继续前进,直到运动员完成才解决其承诺。 It is possible to do that by continually chaining promises, but that seems a very complicated way to do things and certainly not necessary here. 可以通过连续地链接诺言来做到这一点,但这似乎是做事的非常复杂的方式,在这里当然不是必需的。 Further, that isn't at all what your code attempts to do. 此外,这根本不是您的代码尝试执行的操作。 Your code just returns a new promise that is never resolved unless the athelete has already passed the desired distance. 您的代码只会返回一个新的承诺,除非运动员已经通过了期望的距离,否则它将永远无法解决。 If not, it returns a promise that is never resolved or rejected. 如果不是,它将返回从未解决或拒绝的承诺。 This is a fundamental violation of promise concepts. 这是对诺言概念的根本违反。 A function that returns a promise is responsible for eventually resolving or rejecting it unless the calling code expects to just abandon the promise in which case it's probably the wrong design tool. 返回承诺的函数负责最终解决或拒绝承诺,除非调用代码期望放弃承诺,在这种情况下,这可能是错误的设计工具。

Here's another approach that creates a public Athlete() object that has some methods and allows multiple people to be watching the progress: 这是创建公共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