我应该如何“屈服"?在JavaScript中? [英] How should I "yield" in JavaScript?

查看:64
本文介绍了我应该如何“屈服"?在JavaScript中?的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

我应该选择一个还是另一个吗?还是取决于环境(节点,浏览器)?


在评论中询问了

已更新,我想达到什么目的.它是一个简单的可观察对象,其属性更改时会异步触发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));

如果与节点一起运行,则预期输出应为:

before change: old
after change: new
propertyChanged: data, old -> new
all propertyChanged handlers have been called asynchronously

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

解决方案

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

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

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

以下是对每种选择的一些解释-每个选择都可能会满足您的目标,但是每个细节都有所不同.

所有这些选项都允许事件循环的当前滴答声结束,然后它们计划在事件循环的将来滴答声中调用回调.它们的确切不同之处在于何时调用下一个回调,并且根据当前正在处理的事件类型(例如,事件循环在扫描几个不同事件队列的过程中的位置)来调度下一个回调时,它们将有所不同./p>

您可以先阅读以下概述文章节点.js事件循环,计时器和process.nextTick()

process.nextTick(cb)

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

setImmediate(cb)

这计划在事件循环的当前阶段"完成后运行回调.您可以将事件循环视为遍历许多不同类型的队列.当当前正在处理的队列类型为空时,所有待处理的setImmediate()回调都将得到处理.

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

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

相对于您计划的setImmediate()如何处理待处理的setTimeout()取决于您调用setImmediate()时处于事件循环的哪个阶段.这通常超出了代码中应注意的范围.如果像这样的多个异步操作的相对计时很重要,那么编写保证给定序列的代码要安全得多,而不管它们的回调何时启用它们.承诺可以帮助您对诸如此类的事情进行排序.

setTimeout(cb,0)

计时器是事件循环的一个阶段.当它遍历事件循环以查看不同类型的事件队列时,阶段之一是查找时间已过去的所有计时器事件,因此该调用它们的回调了.因此,计时器仅在事件循环处于计时器阶段"时才运行,因此它们相对于其他类型的事件的触发方式是不确定的.当计时器准备就绪时,取决于事件循环在其循环中的位置.就个人而言,除非我试图与其他计时器事件进行同步,否则我通常不使用setTimeout(cb, 0),因为这将保证与其他计时器事件(而非其他类型的事件)的FIFO顺序.

Promise.resolve().then(cb)

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

node.js中的本地承诺确实具有特定的实现.就我个人而言,我不知道您为什么要编写依赖于此特定实现的代码,但是很多人似乎很好奇,所以我会解释.

您可以在本文中看到一个不错的图表:承诺,nextTicks和setImmediates .本机承诺是使用微任务队列来实现的.本质上,它是另一个队列,如nextTick队列,该队列在nextTickQueue之后但在其他任何队列之前进行处理.因此,排队的.then().catch()处理程序将在已调度的调用和nextTick之后以及任何其他类型的事件(计时器,I/O完成等)之前立即运行.

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

I am a bit new to modern JavaScript (ES8). What is a preferred way to yield asynchronously, i.e. continue script execution upon some future iterations of the event loop, using await? I saw the following options:

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");
}

Should I chose one after another or they're all the same? Or maybe it depends on the environment (node, browser)?


Updated, was asked in the comments about what I'm trying to achieve. It is a simple observable object that asynchronously fires propertyChanged event when its properties change. Here is a full example, and the "yielding" piece is inside 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));

If you run it with node, the expected output should be:

before change: old
after change: new
propertyChanged: data, old -> new
all propertyChanged handlers have been called asynchronously

The order of this output matters, i.e., I don't want any event handlers for propertyChanged to be invoked before the setter method for data has returned to the caller.

解决方案

OK, I'll address the new summary of your question in your comments (you should probably edit your question to just say this):

I want to run a piece of code on a future iteration of the event loop in the most efficient way (and let the current method return). No particular preferences, but but the order of continuations should matter. E.g., in my example, if property1 changed, then property2 changed, I first want propertyChanged be fired for property1, then for property2 (in both cases, asynchronously to the code that changed both properties).

The short version is you can use pretty much any of the options below to solve your issue. Without knowing more about your specific situations/requirements, I would probably recommend setImmediate() because it can't starve the event queue if triggered recursively, but either process.nextTick() or Promise.resolve().then() will trigger sooner (before other types of events) if that matters to your caller.

Here's some explanation of each choice - each will likely fulfill your objective, but each differs in some details.

All of these options allow the current tick of the event loop to finish and then they schedule a callback to be called on a future tick of the event loop. They differ in exactly when the next callback will be called and some will vary when they schedule the next callback based upon what type of event is currently be processed (e.g. where the event loop is in it's process of scanning several different event queues).

You can start by reading this overview article The Node.js Event Loop, Timers, and process.nextTick()

process.nextTick(cb)

This is the soonest way to schedule the callback. The current tick of the event loop finishes its execution and then before the node.js event loop code looks at any other event queues in the event loop, it looks for items in the nextTickQueue and runs them. Note, it is possible to "starve" the event loop if you are continually calling process.nextTick() recursively because it does not give other events a chance to run until the nextTickQueue is empty. This is not a "fair" scheduler.

setImmediate(cb)

This schedules a callback to be run after the current "phase" of the event loop is finished. You can think of the event loop as cycling through a number of different types of queues. When the current type of queue that is being processed is empty, then any pending setImmediate() callbacks will get processed.

Note, how this relates to other types of events, then depends upon what type of event was processing when setImmediate() was called.

As an example, if you were in the completion callback from fs.read() and you called setImmediate() to schedule a callback, then the event loop would first process any other pending I/O events before processing your setImmediate() callback. Because it doesn't get called until the event loop advances to the next type of event in the event queue, you can't starve the event loop with setImmediate(). Even recursively call setImmediate() will still cycle through all events.

How a pending setTimeout() is processed relative to a setImmediate() that you schedule depends upon what phase of the event loop you were in when you called the setImmediate(). This is generally beyond the scope of what you should be aware of in your code. If relative timing of multiple async operations like this is important, then you are much safer to just write code that guarantees a given sequence regardless of exactly when they operation is enabled by its callback. Promises can help you sequence things like this.

setTimeout(cb, 0)

Timers are one phase of the event loop. As it goes around the event loop looking at different types of event queues, one of the stages is to look for any timer events whose time has passed and thus it is time to call their callback. Because of this timers only run when the event loop is in the "timer phase" so how they fire relative to other types of events is indeterminate. It depends upon where the event loop is in its cycle when the timer is ready to go. Personally, I generally don't use setTimeout(cb, 0) unless I'm trying to synchronize with other timer events as this will guarantee a FIFO order with other timer events, but not with other types of events.

Promise.resolve().then(cb)

To get to this level of detail for promises (which you normally don't need to), you have to be very aware of what the promise implementation you are using is and how it works. A non-native-code promise implementation will use one of the other timing mechanisms to schedule its .then() handlers. Any of them can appropriately meet the Promise specification so they can vary.

Native promises in node.js do have a specific implementation. Personally, I know of no reason why you should write code that depends upon this specific implementation, but lots of people seem to be curious so I'll explain.

You can see a good diagram in this article: Promises, nextTicks and setImmediates. Native promises are implemented using what's called a micro tasks queue. It's essentially another queue like the nextTick queue that is processed after the nextTickQueue, but before any of the other queues. So, queued up .then() or .catch() handlers run immediately after and nextTick calls that are already scheduled and before any other types of events (timers, I/O completion, etc...).

Non-native promise implementations (like Bluebird or Q) don't have the ability to make a new microTasks queue that is processed after the nextTick queue so they use setImmediate() or process.nextTick().

这篇关于我应该如何“屈服"?在JavaScript中?的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆