如何使用可以启动/停止的recursion函数编写应用程序
我有一个需要不断运行一个function的应用程序。 该函数返回一个承诺。 我希望应用程序一直等到承诺解决之后再次启动该function。
此外,我的应用程序需要一个start
和stop
function,使其开始运行该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...
然后, setTimeout
callback中的代码永远不会被调用。
我可以尝试在我的命令行上启动运行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(...)
将then
callback附加到当前迭代队列的末尾。 由于这是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任务队列的运行优先级比超时任务队列要高。 这就是为什么你的setTimeout
callback永远不会被触发。
如果您使用实际的长时间运行的任务而不是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 }) } }