我应该如何在 JavaScript 中“屈服”?

avo*_*avo 4 javascript promise async-await

我对现代 JavaScript (ES8) 有点陌生。什么是产生异步,即在事件循环的一些未来的迭代继续执行脚本,使用首选的方法await?我看到了以下选项:

async function yield1() {
  await Promise.resolve();
  console.log("done");
}

async function yield2() {
  // setImmediate is non-standard, only Edge and Node have it
  await new Promise(done => (setImmediate? setImmediate: setTimeout)(done));
  console.log("done");
}

async function yield3() {
  await new Promise(done => setTimeout(done));
  console.log("done");
}
Run Code Online (Sandbox Code Playgroud)

我应该一个接一个地选择还是都一样?或者这可能取决于环境(节点、浏览器)?


更新,在评论中被问到我想要实现的目标。它是一个简单的 observable 对象,propertyChanged当其属性改变时异步触发事件。这是一个完整的例子,“屈服”部分在里面firePropertyChanged

const EventEmitter = require('events');

class Model extends EventEmitter {
  constructor(data) {
    super();
    this._data = data;
  }

  get data() {
    return this._data;
  }

  set data(newValue) {
    const oldValue = this._data;
    if (oldValue !== newValue) {
      this._data = newValue;
      this.firePropertyChanged('data', newValue, oldValue);
    }
  }

  async firePropertyChanged(property, newValue, oldValue) {
    await Promise.resolve().then(() =>
      super.emit('propertyChanged', { target: this, property, newValue, oldValue }));
    console.log('all propertyChanged handlers have been called asynchronously');
  }
}

async function waitForChange(obj) {
  await new Promise(resolve => 
    obj.once('propertyChanged', args => 
      console.log(`propertyChanged: ${args.property}, ${args.oldValue} -> ${args.newValue}`)));
}

async function test() {
  const obj = new Model("old");
  var change = waitForChange(obj);
  console.log(`before change: ${obj.data}`);
  obj.data = "new";
  console.log(`after change: ${obj.data}`);
  await change;
}

test().catch(e => console.error(e));
Run Code Online (Sandbox Code Playgroud)

如果你用 node 运行它,预期的输出应该是:

更改前:旧
更改后:新
属性更改:数据,旧 -> 新
所有 propertyChanged 处理程序都已被异步调用

这个输出的顺序很重要,即,我不希望propertyChanged在 setter 方法data返回给调用者之前调用任何事件处理程序。

jfr*_*d00 5

好的,我将在您的评论中解决您的问题的新摘要(您可能应该编辑您的问题以这样说):

我想以最有效的方式在事件循环的未来迭代中运行一段代码(并让当前方法返回)。没有特别的偏好,但继续的顺序应该很重要。例如,在我的示例中,如果 property1 更改,则 property2 更改,我首先希望为 property1 触发 propertyChanged,然后为 property2(在这两种情况下,与更改这两个属性的代码异步)。

简短的版本是您几乎可以使用以下任何选项来解决您的问题。在不了解您的具体情况/要求的情况下,我可能会建议,setImmediate()因为如果递归触发它不能使事件队列挨饿,但是如果这对您的调用者很重要,或者process.nextTick()Promise.resolve().then()将触发(在其他类型的事件之前)。

以下是对每个选择的一些解释 - 每个选择都可能实现您的目标,但每个选择在某些细节上有所不同。

所有这些选项都允许事件循环的当前滴答完成,然后他们安排回调在事件循环的未来滴答上被调用。它们在下一个回调将被调用的确切时间上有所不同,有些会根据当前正在处理的事件类型(例如,事件循环在扫描几个不同的事件队列的过程中所处的位置)调度下一个回调时会有所不同。

您可以先阅读这篇概述文章Node.js 事件循环、计时器和 process.nextTick()

process.nextTick(cb)

这是安排回调的最快方法。事件循环的当前滴答完成其执行,然后在 node.js 事件循环代码查看事件循环中的任何其他事件队列之前,它会在 中查找项目nextTickQueue并运行它们。请注意,如果您不断地process.nextTick()递归调用,则有可能“饿死”事件循环,因为它不会让其他事件有机会运行,直到nextTickQueue为空。这不是一个“公平”的调度程序。

在此处输入图片说明

立即设置(cb)

这将安排在事件循环的当前“阶段”完成后运行回调。您可以将事件循环视为在许多不同类型的队列中循环。当正在处理的当前队列类型为空时,setImmediate()将处理任何挂起的回调。

请注意,这与其他类型的事件有何关联,然后取决于setImmediate()调用时正在处理的事件类型。

例如,如果您在完成回调中fs.read()并且您调用它setImmediate()来安排回调,那么事件循环将在处理您的setImmediate()回调之前首先处理任何其他挂起的 I/O 事件。因为在事件循环前进到事件队列中的下一种事件类型之前它不会被调用,所以你不能用setImmediate(). 即使递归调用setImmediate()仍会循环遍历所有事件。

setTimeout()相对于setImmediate()您安排的,待处理的处理方式取决于您在调用setImmediate(). 这通常超出了您在代码中应该注意的范围。如果像这样的多个异步操作的相对时间很重要,那么只编写保证给定序列的代码更安全,而不管它们的操作何时被其回调启用。Promise 可以帮助您对这样的事情进行排序。

设置超时(CB,0)

定时器是事件循环的一个阶段。当它在事件循环中查看不同类型的事件队列时,其中一个阶段是查找时间已过的任何计时器事件,因此是时候调用它们的回调了。由于这个计时器仅在事件循环处于“计时器阶段”时运行,因此它们如何相对于其他类型的事件触发是不确定的。这取决于计时器准备就绪时事件循环在其周期中的位置。就我个人而言,setTimeout(cb, 0)除非我尝试与其他计时器事件同步,否则我通常不使用它,因为这将保证与其他计时器事件的 FIFO 顺序,但不会与其他类型的事件。

Promise.resolve().then(cb)

要获得 Promise 的这种详细程度(您通常不需要),您必须非常清楚您正在使用的 Promise 实现是什么以及它是如何工作的。非本机代码承诺实现将使用其他计时机制之一来调度其.then()处理程序。它们中的任何一个都可以适当地满足 Promise 规范,因此它们可以有所不同。

node.js 中的原生 promise 确实有特定的实现。就我个人而言,我不知道为什么你应该编写依赖于这个特定实现的代码,但很多人似乎很好奇,所以我会解释一下。

你可以在这篇文章中看到一个很好的图表:Promises、nextTicks 和 setImmediates。本机承诺是使用所谓的微任务队列实现的。它本质上是另一个队列,如 nextTick 队列,在 之后处理nextTickQueue,但在任何其他队列之前。因此,排队.then().catch()处理程序nextTick在任何其他类型的事件(计时器、I/O 完成等)之前和已经调度的调用之后立即运行。

在此处输入图片说明

非本地 promise 实现(如 Bluebird 或 Q)无法创建一个新的 microTasks 队列,该队列在 nextTick 队列之后处理,因此它们使用setImmediate()process.nextTick()