如何使用可以启动/停止的recursion函数编写应用程序

我有一个需要不断运行一个function的应用程序。 该函数返回一个承诺。 我希望应用程序一直等到承诺解决之后再次启动该function。

此外,我的应用程序需要一个startstopfunction,使其开始运行该function,或分别停止它。

这里有一个简单的例子:

 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... 

然后, setTimeoutcallback中的代码永远不会被调用。

我可以尝试在我的命令行上启动运行node ,并直接input到REPL中,但是在我调用a.start() ,terminal冻结了,我不能再input任何东西。

这种事情似乎应该是一个很常见的模式。 例如,Express可让您在没有这些问题的情况下启动/停止服务器。 我需要做些什么来获得这种行为?

你的_run()方法是无限的。 它永远不会停止调用自己,除非其他代码可以运行并更改这个值。但是仅仅使用this.running .then()不足以可靠地允许其他setTimeout()代码运行,因为.then()运行在更高的优先级比事件队列中的计时器事件。

虽然.then()保证是asynchronous的,但它将以比setTimeout()更高的优先级运行,这意味着recursion调用只能无限期地运行,而另一个setTimeout()永远不会运行,因此永远不会改变。

相反,如果你用一个简短的setTimeout()本身recursion调用_run() ,那么你的另一个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 

有关setImmediate()setImmediate()nextTick()之间的相对优先级的讨论,请参阅其他答案。

Promise.resolve()。然后vs setImmediate vs nextTick

关于这个主题的更多信息:

https://github.com/nodejs/node-v0.x-archive/pull/8325


广义的优先级层次似乎是:

 .then() nextTick() other events already in the queue setImmediate() setTimeout() 

所以,你可以看到.then()在队列中的其他事件之前跳转,因此你的setTimeout() nevers只要是一个.then setTimeout()就可以运行。

因此,如果您希望在下次调用this._run()之前允许队列中已有的其他定时器事件运行,则必须使用setImmediate()setTimeout() 。 可能要么在这种情况下工作,但由于其他事件setTimeout() ,我想在这里使用setTimeout()将保证安全,因为你知道一个新的setTimeout()callback不能跳过已经是一个悬而未决事件。

阻止recursion的Promise

这实际上是一个很好的与recursion有关的“阻塞” Promise例子。

您的第一个console.log调用按预期执行。 这是一个同步操作,并且由于Javascript中的运行到完成的调度,它保证在事件循环的当前迭代中运行。

你的第二个console.log是asynchronous的,并且由于setTimeout的实现,它被附加到事件循环的下一个迭代的队列中。 然而,这个下一次迭代是永远不会到达的,因为Promise.resolve().then(...)thencallback附加到当前迭代队列的末尾。 由于这是recursion完成的,所以当前的迭代从未完成。

因此,在事件循环的下一个循环中排队的第二个log从不logging。

使用node.js,您可以通过使用setImmediate实现recursion函数来重现此行为:

 // watch out: this functions is blocking! function rec(x) { return x === Infinity ? x : (console.log(x), setImmediate(rec, x + 1)); } 

通过将setTimeout应用于resolvecallback,Bergi的实现简单地绕过了正常的asynchronousPromise行为。

你的_run方法运行得太快了。 它使用promise并且是asynchronous的,所以你不会得到堆栈溢出或者其他东西,但是promise任务队列的运行优先级比超时任务队列要高。 这就是为什么你的setTimeoutcallback永远不会被触发。

如果您使用实际的长时间运行的任务而不是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 }) } }