我有一个需要连续运行功能的应用。该函数返回一个承诺。我希望应用程序等到承诺解决后才能再次启动该功能。
此外,我的应用程序需要一个start
和stop
函数,使其能够开始运行该函数,或者分别停止它。
我在这里有一个简化的例子:
class App {
constructor() {
this.running = false
this.cycles = 0
}
start() {
this.running = true
this._run()
}
stop() {
this.running = false
}
_run() {
Promise.resolve().then(() => {
this.cycles++
}).then(() => {
if (this.running) {
this._run()
}
})
}
}
module.exports = App
我的问题是,当我使用它时,setTimeout
似乎放弃了我。例如,如果我运行它:
const App = require(" ./ app")
const a = new App()
a.start()
console.log("a.start is not blocking...")
setTimeout(() => {
console.log("This will never get logged")
a.stop()
console.log(a.cycles)
}, 500)
输出将是:
a.start is not blocking...
然后setTimeout
回调中的代码永远不会被调用。
我可以尝试在命令行上开始运行node
并直接输入REPL,但在我调用a.start()
后终端冻结,我再也无法输入任何内容。
这种事情似乎应该是一种非常常见的模式。例如,Express允许您在没有这些问题的情况下启动/停止服务器。我需要做些什么才能获得这种行为?
答案 0 :(得分:1)
您的_run()
方法是无限的。它永远不会停止调用自己,除非其他代码可以运行并更改this.running
的值,但仅使用.then()
不足以可靠地允许您的其他setTimeout()
代码运行,因为{{1}运行的优先级高于事件队列中的计时器事件。
虽然.then()
保证是异步的,但它将以比.then()
更高的优先级运行,这意味着您的递归调用只是无限期地运行而您的其他setTimeout()
永远不会运行,因此setTimeout()
永远不会改变。
相反,如果您使用短this.running
本身递归调用_run()
,那么您的其他setTimeout()
将有机会投放。而且,由于根本不需要在那里使用promises,你可以删除它们:
将其更改为:
setTimeout()
有关class App {
constructor() {
this.running = false
this.cycles = 0
}
start() {
this.running = true
this._run()
}
stop() {
this.running = false
}
_run() {
this.cycles++
if (this.running) {
// call recursively after a short timeout to allow other code
// a chance to run
setTimeout(this._run.bind(this), 0);
}
}
}
module.exports = App
,.then()
和setImmediate()
之间相对优先级的一些讨论,请参阅此其他答案:
Promise.resolve().then vs setImmediate vs nextTick
有关此主题的更多信息:
https://github.com/nodejs/node-v0.x-archive/pull/8325
广义优先级层次结构似乎是:
nextTick()
因此,您可以从中看到.then()
nextTick()
other events already in the queue
setImmediate()
setTimeout()
在队列中已有的其他事件之前跳转,因此只要.then()
等待setTimeout()
,.then()
就会运行去吧。
因此,如果您希望在下次调用this._run()
之前允许队列中已有其他计时器事件运行,则必须使用setImmediate()
或setTimeout()
。可能要么在这种情况下工作,但由于其他事件是setTimeout()
,我认为在这里使用setTimeout()
可以保证安全,因为你知道新的setTimeout()
回调不能跳到前面一个已经是未决事件。
答案 1 :(得分:1)
Promise
s 这实际上是一个很好的例子来阻止" Promise
与递归有关。
您的第一个console.log
调用按预期执行。这是一个同步操作,由于Javascript中的run-to-completion调度,它保证在event loop的当前迭代中运行。
您的第二个console.log
是异步的,由于setTimeout
的实现,它附加到事件循环的下一次迭代的队列中。但是,永远不会达到下一次迭代,因为Promise.resolve().then(...)
将then
回调附加到当前迭代的队列末尾。由于这是递归完成的,因此当前的迭代永远不会完成。
因此,您在事件循环的下一轮排队的第二个log
永远不会记录。
使用node.js,您可以通过使用setImmediate
实现递归函数来重现此行为:
// watch out: this functions is blocking!
function rec(x) {
return x === Infinity
? x
: (console.log(x), setImmediate(rec, x + 1));
}
Bergi的实现只是通过将Promise
应用于解析回调来绕过正常的异步setTimeout
行为。
答案 2 :(得分:0)
您的_run
方法运行得太快了。它使用promises并且是异步的,因此您不会获得堆栈溢出或其他内容,但promise任务队列的运行优先级高于超时任务队列。这就是为什么你的setTimeout
回调永远不会被解雇的原因。
如果您使用实际的长时间运行任务而不是Promise.resolve()
,那么它将起作用。
class App {
constructor() {
this.running = false
this.cycles = 0
}
start() {
if (this.running) {
return Promise.reject(new Error("Already running"))
} else {
this.running = true
return this._run()
}
}
stop() {
this.running = false
}
_run() {
return new Promise(resolve => {
this.cycles++
setTimeout(resolve, 5) // or something
}).then(() => {
if (this.running)
return this._run()
else
return this.cycles
})
}
}