簡體   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