如何使用可以启动/停止的递归功能编写应用程序

时间:2016-10-06 21:38:53

标签: javascript node.js asynchronous recursion promise

我有一个需要连续运行功能的应用。该函数返回一个承诺。我希望应用程序等到承诺解决后才能再次启动该功能。

此外,我的应用程序需要一个startstop函数,使其能够开始运行该函数,或者分别停止它。

我在这里有一个简化的例子:

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允许您在没有这些问题的情况下启动/停止服务器。我需要做些什么才能获得这种行为?

3 个答案:

答案 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
    })
  }
}