在JavaScript中如何使用“yield”?

6

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

我应该一个接一个地选择它们还是它们都一样?或者可能取决于环境(节点、浏览器)?
< p >更新,评论中被问到我想要实现什么。这是一个简单的可观察对象,当其属性更改时异步触发 propertyChanged 事件。这里是一个完整的示例,而“yielding”部分在 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运行它,期望的输出应该是:
更改前: 旧值
更改后: 新值
属性已更改: data, 旧值 -> 新值
所有propertyChanged处理程序都已异步调用
此输出顺序很重要,即我不希望在setter方法返回给调用者之前调用任何propertyChanged事件处理程序。

2
你是否知道 setImmediate 没有标准化?因为这个原因,你可能会想要排除它。 - James Thorpe
2
有趣的是,MDN将其列为“大多数浏览器尚未实现”的内容。 (https://developer.mozilla.org/en-US/docs/Web/API/Window/setImmediate#Browser_compatibility) - James Thorpe
1
rxjs是一个响应式JavaScript库。你可以创建可观察对象,这会让你感觉像在使用Python中的生成器一样。https://rxjs-dev.firebaseapp.com/ - Joey Gough
你实际上想要解决什么问题?你能给我们展示一个你试图使用它并在其中尝试做什么的例子吗?JavaScript通常不考虑“yielding”的概念。你启动一个异步操作,然后将控制返回到事件循环。当异步操作完成时,你会在完成回调中恢复逻辑流程。这也可以使用promises和await来完成。 - jfriend00
另外,如果是这种情况(只想在当前事件循环完成执行后运行某些内容),那么有多种方法可以实现,你选择哪种取决于在下一段代码(你要排队等待的地方)之前想要运行什么其他操作。对此有任何特定偏好吗?你希望它排队等待时是在队列的最前面、最末尾、在任何未处理的I/O事件之前还是之后呢? - jfriend00
显示剩余5条评论
1个回答

12

好的,我会在你的评论中回答您关于问题的最新总结(您可能需要编辑您的问题只说这个):

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

简而言之,您可以使用以下任何选项解决您的问题。如果递归触发,则不会饿死事件队列,因此我通常建议setImmediate(),但是如果对调用方很重要,则process.nextTick()Promise.resolve().then()将更早地触发。

以下是每个选择的一些说明-每个都可能实现您的目标,但每个细节有所不同。

所有这些选项都允许事件循环的当前刻度完成,然后计划在事件循环的未来时刻调用回调。它们的不同之处在于下一个回调的确切调用时间,某些选项会根据当前正在处理的事件类型(例如,事件循环正在扫描几个不同的事件队列时)而变化下一个回调的安排时间。

您可以从阅读此概述文章开始 Node.js事件循环,计时器和process.nextTick()

process.nextTick(cb)

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

enter image description here

setImmediate(cb)

这个函数会安排一个回调在当前事件循环的“阶段”结束后运行。你可以把事件循环看作是在不同类型的队列之间循环。当正在处理的队列为空时,任何待处理的setImmediate()回调都将被处理。

需要注意的是,这与其他类型的事件的关系取决于调用setImmediate()时正在处理哪种事件。

例如,如果你在fs.read()的完成回调中调用了setImmediate()来安排一个回调,那么在处理你的setImmediate()回调之前,事件循环会先处理任何其他待处理的I/O事件。因为直到事件循环进入事件队列中的下一个类型的事件时才会调用它,所以你无法使用setImmediate()饿死事件循环。即使递归调用setImmediate()也仍然会循环遍历所有事件。

如何处理待处理的setTimeout()相对于你安排的setImmediate()取决于你调用setImmediate()时处于事件循环的哪个阶段。这通常超出了你在代码中应该意识到的范围。如果多个异步操作的相对时间很重要,那么你最好编写能够保证给定顺序的代码,而不管它们的回调何时启用。Promise可以帮助你序列化这样的操作。

setTimeout(cb, 0)

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

Promise.resolve().then(cb)

要深入了解Promise的细节(通常情况下不需要),你需要非常了解所使用的Promise实现及其工作原理。非本地代码的Promise实现将使用其他时间机制来调度其.then()处理程序。任何一种时间机制都可以适当地满足Promise规范,因此它们可能会有所不同。
Node.js中的本地Promise确实有一个特定的实现。就个人而言,我不知道为什么您应该编写依赖于此特定实现的代码,但很多人似乎很好奇,所以我将解释一下。
您可以在这篇文章Promises, nextTicks and setImmediates中看到一个很好的图表。本地Promise使用所谓的微任务队列实现。它实际上是另一个队列,类似于nextTick队列,在nextTickQueue之后但在其他任何队列之前处理。因此,排队的.then()或.catch()处理程序会在已经计划的nextTick调用之后立即运行,并在任何其他类型的事件(定时器、I/O完成等)之前运行。

enter image description here

非原生的Promise实现(例如Bluebird或Q)没有能力创建一个新的microTasks队列,在nextTick队列之后处理,因此它们使用setImmediate()process.nextTick()


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接