【问题标题】:How to resolve recursive asynchronous promises?如何解决递归异步承诺?
【发布时间】:2015-12-08 03:07:25
【问题描述】:

我正在使用 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?

而不是

resolve(athlete);

我可以的

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

但如果可能的话,我想避免这种情况,我真的很想能够写作

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

【问题讨论】:

  • 这段代码真的很难理解,但看起来它确实错误地使用了 Promise。除非我完全误解了这一点,否则我认为你应该花点时间重新考虑一下这个设计,也许先阅读一下 Promise。
  • 您需要了解每个.then() 只执行一次,并且promise 会得到解决或拒绝一次并且永远。因此,当您第一次到达checkIsFinished 时,您会返回一个待处理的承诺,然后使用setTimeout 再次递归调用自己。但是这个新的承诺无处可去,因为setTimeout 只是返回一个句柄,而不是一个承诺,所以这个新的承诺也保持未决,然后你重复 - 可能导致内存泄漏。花时间阅读并理解 Javascript 事件循环的精妙之处。这将为您节省大量时间和挫败感。
  • 当您执行setTimeout(checkIsFinished.bind(null, athlete), 1000); 时,您会在 new Promise 中创建,所以旧的promise 永远不会解决
  • @Amit,我已经编辑了我的问题以澄清情况。
  • @caasjj,@Grundy,感谢 cmets。我知道setTimeout(checkIsFinished.bind(null, athlete), 1000) 创建了一个新的承诺,而最初的承诺“丢失”了。我的问题基本上是有办法解决这个问题吗?我已经编辑了问题以试图澄清情况。

标签: javascript asynchronous recursion promise es6-promise


【解决方案1】:

错误是您正在传递一个向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. 仅处理返回承诺的异步函数。
  2. 当没有返回一个 Promise 时,用 Promise 构造函数包装它。
  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);
&lt;div id="div"&gt;&lt;/div&gt;

请注意,checkIsFinished 返回运动员或承诺。这在这里很好,因为.then 函数会自动将您传入的函数的返回值提升为 Promise。如果您将在其他情况下调用checkIsFinished,您可能希望自己进行促销,使用return Promise.resolve(athlete); 而不是return athlete;。

根据 Amit 的 cmets 进行编辑:

对于非递归答案,将整个 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);
&lt;div id="div"&gt;&lt;/div&gt;

【讨论】:

  • 虽然您的“最佳实践”列表看起来很有能力,但它会将您带入一个场景,您可以递归地创建新的 Promise,而不是链接,而是构建更深更深的“Promise 堆栈”。每个 wait(1000).then(...) 都在托管一个新的 Promise - 类似于 wait(x).then(()=&gt;wait(x).then(()=&gt;wait(x).then(...)))。如果/何时以及最终结果到达,这将对内存和性能产生不利影响。更糟糕的是,所有这些细节都隐藏在华而不实的语法和抽象中。
  • @Amit 你错了。首先,这里没有“堆栈”递归,因为不是在同一次运行中添加东西。没有提前构建链条。正在发生的一切是,每一秒都会创建一个新的 Promise,就像在您的示例中一样,同时,对前一个 Promise 的所有引用都消失了,使其可被垃圾回收。这可以永远持续下去,而不会出现任何内存问题。没有像您想象的那样构建“链”。我在这里首选的解决方案是使用setInterval,但你的问题要求“递归”,所以你去吧。
  • 不确定您指的是我的什么问题,但由于我在之前的评论中解释过,因此无法收集承诺。每个 Promise 都被解析,然后一个 .then() 被调用,回调函数返回一个新的 Promise。 then 调用最终将解析为内部 promise 解析的任何内容,但在它完成之前,它就在那里,它正在等待。并且“递归”地继续下去。事情不会提前发生的事实是无关紧要的,因为如果最终事件永远不会到达,您将创建承诺,直到内存耗尽。
  • @Amit 我非常相信 promise 实现可以优化这一点,正如 this answer to a related question 中提到的那样,但这里没有空间分享我的分析。但是,我承认我不知道是否有任何实现正在这样做,但我会尝试找出答案。 - 尽管您的问题的标题是“如何解决递归异步承诺?”,但我已经编辑了我的答案以提供一个非递归答案,希望能满足您对内存使用的担忧。
  • 我很抱歉@Amit,我让你对 OP 感到困惑,你可能对现有的实现是正确的。感谢您的反馈,我编辑了 waitUntil 函数以使其更清晰。刚刚注意到你的回答。
【解决方案2】:

使用 setTimeout / setInterval 是与 Promise 不兼容的场景之一,并导致您使用皱眉的 Promise 反模式。

话虽如此,如果您重构您的函数,使其成为“等待完成”类型的函数(并相应地命名),您将能够解决您的问题。 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 套接字。

【讨论】:

  • 我同意基于套接字的解决方案会更好。不幸的是,目前这是不可能的。
  • 您的解决方案似乎不起作用。当我运行代码 sn-p 时,我在控制台中看到多条 updated athlete Object { timeTaken: 2.5, distanceTravelled: 25 } 形式的消息,但没有 not finished yet 消息和 finished 消息。
  • @user5325596 - 运行几次。我有意添加了一个随机抛出异常的代码,显示此解决方案如何处理异常(这可能发生在您的真实代码中)。抛出此类异常时,您将看不到finished。您应该清理此代码部分以供实际使用。 (相关线路为if(Date.now()%1000 &lt; 250))
  • @user5325596 - 顺便说一句,您应该知道,至少根据我的理解(我对此非常有信心),您当前接受的答案中的解决方案都缺乏异常处理(尝试在其中抛出)或正在产生内存/性能问题。您应该考虑使用它们的后果。
  • @Amit - 请注意,.then 函数和 promise 构造函数执行器函数中的异常会自动导致拒绝,因此您的异常处理是多余的。接受的答案具有足够的错误处理,因为startRunning 的同步部分内的任何编码错误都将被 JS 抛出到控制台,并且随后的承诺链以报告错误的.catch 正确终止。接受的答案也有一个没有性能争议的非递归解决方案。
【解决方案3】:

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

Promises 是一次性状态机。因此,您返回一个 Promise,并且在未来的某个时间,该 Promise 既可以被拒绝,也可以通过值解决。鉴于承诺的设计,我认为有意义的是可以像这样使用的东西:

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/


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


以下是您在使用 Promise 时犯的一些基本错误:

  1. 它们是一次性对象。它们只会被解决或拒绝一次。
  2. startRunning().then(checkIsFinished) 的结构没有逻辑意义。要使第一部分起作用,startRunning() 必须返回一个 Promise,并且它必须在发生有用的事情时解决或拒绝该 Promise。你只是在两秒钟后解决它,这似乎并没有完成任何有用的事情。
  3. 您的描述听起来像是您希望 `checkIsFinished() 继续执行,而不是在运动员完成之前履行其承诺。可以通过不断地链接承诺来做到这一点,但这似乎是一种非常复杂的做事方式,在这里当然没有必要。此外,这根本不是您的代码试图做的事情。您的代码只会返回一个永远不会解决的新承诺,除非运动员已经通过了所需的距离。如果不是,它会返回一个从未解决或拒绝的承诺。这是对承诺概念的根本违反。返回 promise 的函数负责最终解决或拒绝它,除非调用代码期望放弃 promise,在这种情况下,它可能是错误的设计工具。

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

【讨论】:

  • 感谢您的回答。我已经编辑了我的问题以使事情更清楚。所编写的代码只是试图模仿运动员实际在服务器上的真实场景。
  • @user5325596 - 下一次,请描述您的实际情况,这样我们就不会浪费太多时间来解决错误的问题 - 非常令人沮丧!那么,您是否希望 checkIsFinished() 返回一个承诺,该承诺在实际完成时已解决,并且将通过反复调用 ajax 来轮询 checkIsFinished 内部的服务器以查看它实际完成的时间来确定?
  • @user5325596 - 抱歉,您对这个问题的参与太少(回复之间的天数),我无法继续进一步研究。
猜你喜欢
  • 2016-12-23
  • 2023-03-30
  • 2018-05-28
  • 2019-04-13
  • 2018-11-03
  • 2019-03-10
  • 2020-07-04
  • 2019-03-19
  • 1970-01-01
相关资源
最近更新 更多