【问题标题】:How should I "yield" in JavaScript?我应该如何在 JavaScript 中“屈服”?
【发布时间】:2018-11-22 12:35:09
【问题描述】:

我对现代 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");
}

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


更新,在 cmets 中被问及我想要实现的目标。它是一个简单的可观察对象,当其属性发生变化时会异步触发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));

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

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

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

【问题讨论】:

  • 您知道setImmediate 没有标准化吗?你可能想打折那个,因为它本身。
  • @JamesThorpe,不,我不是,感谢您指出。不过,它似乎适用于所有现代浏览器和 Node(IE11 是不可能的)。
  • 有趣 - MDN 将其列为mostly not implemented across the board
  • rxjs 是一个反应式 js 库。你可以创建 observables,这对于在 python 中使用生成器会感觉很熟悉。 rxjs-dev.firebaseapp.com
  • @JamesThorpe,你是对的!只有 Edge 和 Node 有setImmediate。剩下的只有 #1 和 #3。

标签: javascript promise async-await


【解决方案1】:

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

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

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

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

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

您可以先阅读这篇概述文章The Node.js Event Loop, Timers, and process.nextTick()

process.nextTick(cb)

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

setImmediate(cb)

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

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

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

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

setTimeout(cb, 0)

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

Promise.resolve().then(cb)

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

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

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

非原生 Promise 实现(如 Bluebird 或 Q)无法创建在 nextTick 队列之后处理的新 microTasks 队列,因此它们使用 setImmediate()process.nextTick()

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-05-03
    • 2019-06-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2018-05-02
    相关资源
    最近更新 更多