我正在玩承诺,我遇到了异步递归承诺的问题。
场景是运动员开始跑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)
答案 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(因为混合承诺代码和非承诺代码不可避免地导致这样的错误。)
在此之后,我发现代码更易于推理并且更难以进行操作,因为所有内容都遵循相同的模式。
将此应用到您的示例中让我在这里(为了简洁,我使用的是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/
仅供参考,我的设计偏好可能是拥有一个公共运动员对象,然后该对象的方法开始运行,停止运行等...
以下是使用承诺时遇到的一些基本问题:
startRunning().then(checkIsFinished)
只是没有逻辑意义。对于这个工作的第一部分,startRunning()
必须返回一个承诺,它必须解决矿石拒绝承诺当有用的事情。你只需要在两秒钟之后解决它,这似乎没有什么用处。这是另一种创建公共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");
});