Как мне «уступить» в JavaScript? - PullRequest
0 голосов
/ 16 ноября 2018

Я немного новичок в современном 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

Порядок этих выходных данных имеет значение, т. Е. Я не хочу, чтобы какие-либо обработчики событий для propertyChanged вызывались до того, как метод установщика для data вернется к вызывающей стороне.

1 Ответ

0 голосов
/ 22 ноября 2018

Хорошо, я рассмотрю новое резюме вашего вопроса в ваших комментариях (вам, вероятно, следует отредактировать свой вопрос, чтобы просто сказать это):

Я хочу запустить фрагмент кода набудущая итерация цикла событий наиболее эффективным способом (и пусть текущий метод возвращается).Никаких особых предпочтений, но порядок продолжения должен иметь значение.Например, в моем примере, если свойство1 изменилось, то свойство2 изменилось, я сначала хочу, чтобы propertyChanged был запущен для свойства1, а затем для свойства2 (в обоих случаях асинхронно с кодом, который изменил оба свойства).

Короткая версия - вы можете использовать практически любой из приведенных ниже вариантов для решения вашей проблемы.Не зная больше о ваших конкретных ситуациях / требованиях, я бы, вероятно, порекомендовал 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().Поскольку он не вызывается до тех пор, пока цикл событий не перейдет к следующему типу события в очереди событий, вы не можете заморозить цикл событий с setImmediate().Даже рекурсивный вызов setImmediate() все равно будет циклически проходить через все события.

То, как обрабатывается ожидающий setTimeout() относительно setImmediate(), который вы запланировали, зависит от того, на какой фазе цикла событий вы находились, когда вы вызывалиsetImmediate().Как правило, это выходит за рамки того, что вы должны знать в своем коде.Если важна относительная синхронизация нескольких асинхронных операций, подобных этой, то гораздо безопаснее просто написать код, который гарантирует заданную последовательность независимо от того, когда именно эта операция включена ее обратным вызовом.Обещания могут помочь вам упорядочить такие вещи.

setTimeout (cb, 0)

Таймеры являются одной фазой цикла событий. Поскольку он обходит цикл обработки событий, рассматривая различные типы очередей событий, одним из этапов является поиск любых событий таймера, время которых прошло, и, следовательно, пришло время вызвать их обратный вызов. Из-за этого таймеры работают только тогда, когда цикл событий находится в «фазе таймера», поэтому то, как они запускаются относительно других типов событий, является неопределенным. Это зависит от того, где находится цикл обработки событий, когда таймер готов к работе. Лично я обычно не использую setTimeout(cb, 0), если не пытаюсь синхронизироваться с другими событиями таймера, поскольку это гарантирует порядок FIFO с другими событиями таймера, но не с другими типами событий.

Promise.resolve (). Затем (Си-Би)

Чтобы достичь этого уровня детализации для обещаний (которые вам обычно не нужны), вы должны быть очень осведомлены о том, что реализация обещаний вы используете и как она работает. Реализация обещаний, не связанных с собственным кодом, будет использовать один из других механизмов синхронизации для планирования своих обработчиков .then(). Любой из них может соответствующим образом соответствовать спецификации Promise, поэтому они могут различаться.

Собственные обещания в node.js имеют конкретную реализацию. Лично я не знаю причин, почему вы должны писать код, который зависит от этой конкретной реализации, но многим людям, кажется, любопытно, поэтому я объясню.

В этой статье вы можете увидеть хорошую диаграмму: Promises, nextTicks и setImmediates . Собственные обещания реализуются с использованием так называемой очереди микро-задач. По сути, это другая очередь, такая как очередь nextTick, которая обрабатывается после nextTickQueue, но перед любой из других очередей. Таким образом, обработчики .then() или .catch() в очереди запускаются сразу после и nextTick вызовов, которые уже запланированы и перед любыми другими типами событий (таймеры, завершение ввода-вывода и т. Д.).

enter image description here

Реализации, не относящиеся к нативным обещаниям (например, Bluebird или Q), не имеют возможности создать новую очередь микрозадач, которая обрабатывается после очереди nextTick, поэтому они используют setImmediate() или process.nextTick().

...