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

Javascript 如何使用可以启动/停止的递归函数编写应用程序,javascript,node.js,asynchronous,recursion,promise,Javascript,Node.js,Asynchronous,Recursion,Promise,我有一个应用程序,需要连续运行功能。该函数返回一个承诺。我希望应用程序在再次启动功能之前,等待承诺得到解决 此外,我的应用程序需要一个启动和停止功能,使其分别启动或停止该功能 我这里有一个简化的例子: class App { constructor() { this.running = false this.cycles = 0 } start() { this.running = true this._run() } 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
似乎放弃了我。例如,如果我运行以下命令:

常量应用程序=需要(“./应用程序”)

输出将是:

a.start is not blocking...
然后,
setTimeout
回调中的代码永远不会被调用

我可以尝试在命令行上开始运行
节点
,并直接在REPL中键入,但在调用
a.start()
后,终端冻结,我无法再键入任何内容

这类事情似乎应该是一种相当普遍的模式。例如,Express允许您在没有这些问题的情况下启动/停止服务器。我需要做什么才能获得这种行为?

您的
\u run()
方法是无限的。除非其他代码可以运行并更改
this的值,否则它永远不会停止调用自身。运行
但仅使用
.then()
不足以可靠地允许其他
setTimeout()
代码运行,因为
.then()
以高于事件队列中计时器事件的优先级运行

虽然
.then()
保证是异步的,但它将以比
setTimeout()
更高的优先级运行,这意味着您的递归调用将无限期运行,而您的另一个
setTimeout()
将永远无法运行,因此
此.running
永远不会更改

相反,如果您使用较短的
setTimeout()
本身递归调用
\u 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
有关
.then()
setImmediate()
nextTick()
之间的相对优先级的讨论,请参见此其他答案:

有关此主题的更多信息,请参阅:


广义优先级层次结构似乎是:

.then()
nextTick()
other events already in the queue
setImmediate()
setTimeout()
因此,您可以从中看到,
.then()
跳到队列中已经存在的其他事件之前,因此您的
setTimeout()
只要它们是一个
.then()
等待执行,就不会运行


因此,如果要在下次调用
this.\u run()
之前允许队列中已有的其他计时器事件运行,则必须使用
setImmediate()
setTimeout()
。在这种情况下,这两种方法都可能有效,但由于其他事件是
setTimeout()
,我认为在这里使用
setTimeout()
可以保证安全,因为您知道新的
setTimeout()
回调不能跳到已经是挂起事件的回调之前。

您的
\u run
方法运行得太快了。它使用承诺并且是异步的,因此不会出现堆栈溢出或其他情况,但是承诺任务队列的优先级高于超时任务队列。这就是您的
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
    })
  }
}
阻止递归
Promise
s 这实际上是一个与递归相关的“阻塞”
Promise
的好例子

第一次
console.log
调用按预期执行。这是一个同步操作,由于Javascript中的调度,它保证在当前的迭代中运行

您的第二个
控制台.log
是异步的,由于
setTimeout
的实现,它被附加到事件循环的下一次迭代的队列中。但是,由于
Promise.resolve()。然后(…)
then
回调附加到当前迭代的队列末尾,因此永远不会到达下一个迭代。因为这是递归完成的,所以当前迭代永远不会完成

因此,在事件循环的下一轮排队的第二个
日志
,从不记录日志

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

// watch out: this functions is blocking!

function rec(x) {
  return x === Infinity
   ? x
   : (console.log(x), setImmediate(rec, x + 1));
}

Bergi的实现通过将
setTimeout
应用于resolve回调来绕过正常的异步
Promise
行为。

这种类型的应用程序称为“服务”。我知道在.Net世界中,您可以创建一个“Windows服务”类型的项目,然后将其注册为任何计算机上的服务,使用控制面板/管理工具/服务停止/启动它。我想你应该调查一下。
// watch out: this functions is blocking!

function rec(x) {
  return x === Infinity
   ? x
   : (console.log(x), setImmediate(rec, x + 1));
}