如何解决递归异步承诺?
How to resolve recursive asynchronous promises?
我正在玩弄 promise,但我在处理异步递归 promise 时遇到了麻烦。
场景是一名运动员开始 运行 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)
由于您对 promises 的使用非常不合时宜,因此很难准确判断您正在尝试做什么或哪种实施最适合,但这里有一个建议。
Promises 是一次性状态机。因此,您 return 一个承诺,并且恰好在未来的某个时间,该承诺可以被拒绝并提供价值。鉴于承诺的设计,我认为有意义的是可以像这样使用的东西:
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/
仅供参考,我的设计偏好可能是有一个 public 运动员对象,然后在该对象上启动 运行、停止 运行 等方法...
以下是您在使用 promises 时犯的一些基本错误:
- 它们是一次性对象。他们只被解决或拒绝一次。
- 结构
startRunning().then(checkIsFinished)
只是没有逻辑意义。为了使第一部分起作用,startRunning()
必须 return 一个承诺,并且它必须在有用的事情发生时解决或拒绝该承诺。你只是在两秒钟后解决它,这似乎并没有真正完成任何有用的事情。
- 您的描述听起来像是您希望 `checkIsFinished() 继续执行,直到运动员完成后才兑现其承诺。可以通过不断地链接承诺来做到这一点,但这似乎是一种非常复杂的做事方式,当然在这里没有必要。此外,这根本不是您的代码试图做的。您的代码只是 return 一个新的承诺,除非运动员已经超过所需的距离,否则永远不会解决。如果不是,它 return 是一个永远不会被解决或拒绝的承诺。这是对承诺概念的根本违反。 return 承诺的函数负责最终解决或拒绝它,除非调用代码期望放弃承诺,在这种情况下它可能是错误的设计工具。
这是另一种创建 public 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");
});
错误在于您正在传递一个 return 对 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(因为混合承诺代码和非承诺代码不可避免地会导致这样的错误)。
我遵循的避免此类错误的最佳做法:
- 只处理 return 承诺的异步函数。
- 当没有 return 承诺时,用承诺构造函数包装它。
- 尽可能窄地(用尽可能少的代码)包装它。
- 不要将 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
return 要么是运动员,要么是诺言。这在这里很好,因为 .then
函数会自动从您传递给 promise 的函数中提升 return 值。如果您将在其他情况下调用 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>
使用 setTimeout
/ setInterval
是不能很好地处理 promise 的场景之一,会导致您使用皱眉的 promise 反模式。
话虽如此,如果您重构您的函数,使其成为 "wait for completion" 类型的函数(并相应地命名),您将能够解决您的问题。 waitForFinish
函数只被调用一次,并且 returns 一个单一的承诺(尽管是一个新的,在 startRunning
中创建的原始承诺之上)。通过 setTimeout
的重复处理是在内部轮询函数中完成的,其中使用适当的 try/catch 来确保将异常传播到承诺中。
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);
虽然所有这些代码都可以正常运行,但从不建议在异步环境中使用轮询解决方案,应尽可能避免使用。在您的情况下,由于此示例模拟与服务器的通信,因此我会考虑尽可能使用网络套接字。
我正在玩弄 promise,但我在处理异步递归 promise 时遇到了麻烦。
场景是一名运动员开始 运行 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)
由于您对 promises 的使用非常不合时宜,因此很难准确判断您正在尝试做什么或哪种实施最适合,但这里有一个建议。
Promises 是一次性状态机。因此,您 return 一个承诺,并且恰好在未来的某个时间,该承诺可以被拒绝并提供价值。鉴于承诺的设计,我认为有意义的是可以像这样使用的东西:
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/
仅供参考,我的设计偏好可能是有一个 public 运动员对象,然后在该对象上启动 运行、停止 运行 等方法...
以下是您在使用 promises 时犯的一些基本错误:
- 它们是一次性对象。他们只被解决或拒绝一次。
- 结构
startRunning().then(checkIsFinished)
只是没有逻辑意义。为了使第一部分起作用,startRunning()
必须 return 一个承诺,并且它必须在有用的事情发生时解决或拒绝该承诺。你只是在两秒钟后解决它,这似乎并没有真正完成任何有用的事情。 - 您的描述听起来像是您希望 `checkIsFinished() 继续执行,直到运动员完成后才兑现其承诺。可以通过不断地链接承诺来做到这一点,但这似乎是一种非常复杂的做事方式,当然在这里没有必要。此外,这根本不是您的代码试图做的。您的代码只是 return 一个新的承诺,除非运动员已经超过所需的距离,否则永远不会解决。如果不是,它 return 是一个永远不会被解决或拒绝的承诺。这是对承诺概念的根本违反。 return 承诺的函数负责最终解决或拒绝它,除非调用代码期望放弃承诺,在这种情况下它可能是错误的设计工具。
这是另一种创建 public 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");
});
错误在于您正在传递一个 return 对 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(因为混合承诺代码和非承诺代码不可避免地会导致这样的错误)。
我遵循的避免此类错误的最佳做法:
- 只处理 return 承诺的异步函数。
- 当没有 return 承诺时,用承诺构造函数包装它。
- 尽可能窄地(用尽可能少的代码)包装它。
- 不要将 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
return 要么是运动员,要么是诺言。这在这里很好,因为 .then
函数会自动从您传递给 promise 的函数中提升 return 值。如果您将在其他情况下调用 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>
使用 setTimeout
/ setInterval
是不能很好地处理 promise 的场景之一,会导致您使用皱眉的 promise 反模式。
话虽如此,如果您重构您的函数,使其成为 "wait for completion" 类型的函数(并相应地命名),您将能够解决您的问题。 waitForFinish
函数只被调用一次,并且 returns 一个单一的承诺(尽管是一个新的,在 startRunning
中创建的原始承诺之上)。通过 setTimeout
的重复处理是在内部轮询函数中完成的,其中使用适当的 try/catch 来确保将异常传播到承诺中。
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);
虽然所有这些代码都可以正常运行,但从不建议在异步环境中使用轮询解决方案,应尽可能避免使用。在您的情况下,由于此示例模拟与服务器的通信,因此我会考虑尽可能使用网络套接字。