Конструктор пользовательского класса обещаний вызывается дважды (расширение стандартного обещания) - PullRequest
0 голосов
/ 05 ноября 2018

Я играю с Promise Extensions для JavaScript (prex) и хочу расширить стандартный Promise класс с поддержкой отмены с использованием prex.CancellationToken , полный код здесь .

Неожиданно я вижу, как конструктор моего пользовательского класса CancellablePromise вызывается дважды. Чтобы упростить ситуацию, я сократил всю логику отмены и оставил лишь минимум, необходимый для повторения проблемы:

class CancellablePromise extends Promise {
  constructor(executor) {
    console.log("CancellablePromise::constructor");
    super(executor);
  }
}

function delayWithCancellation(timeoutMs, token) {
  // TODO: we've stripped all cancellation logic for now
  console.log("delayWithCancellation");
  return new CancellablePromise(resolve => {
    setTimeout(resolve, timeoutMs);
  }, token);
}

async function main() {
  await delayWithCancellation(2000, null);
  console.log("successfully delayed.");
}

main().catch(e => console.log(e));

Запуск с node simple-test.js, я получаю это:

delayWithCancellation
CancellablePromise::constructor
CancellablePromise::constructor
successfully delayed.

Почему существует два вызова CancellablePromise::constructor?

Я попытался установить точки останова с помощью VSCode. Трассировка стека для второго попадания показывает, что он вызывается из runMicrotasks, который сам вызывается из _tickCallback где-то внутри узла.

Обновлено , у Google теперь есть «ожидайте под капотом» сообщение в блоге, которое является хорошим чтением, чтобы понять это поведение и некоторые другие особенности реализации асинхронного / ожидающего выполнения в V8.

1 Ответ

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

Первое обновление:

Сначала я подумал, .catch( callback) после того, как 'main' вернет новое ожидающее обещание расширенного класса Promise, но это неверно - вызов асинхронной функции возвращает обещание Promise.

Сокращение кода, чтобы получить только ожидающее обещание:

class CancellablePromise extends Promise {
  constructor(executor) {
    console.log("CancellablePromise::constructor");
    super(executor);
  }
}

async function test() {
   await new CancellablePromise( ()=>null);
}
test();

показывает, что расширенный конструктор вызывается дважды в Firefox, Chrome и Node.

Теперь await вызывает Promise.resolve для своего операнда. (Правка: или, возможно, это было в ранних версиях Jyn Engine async / await, которые не были строго реализованы в стандарте)

Если операнд является обещанием, конструктор которого - Promise, Promise.resolve возвращает операнд без изменений.

Если операнд является доступным, конструктор которого не является Promise, Promise.resolve вызывает метод then операнда с обработчиками как onfulfilled, так и onRejected, чтобы получить уведомление об установившемся состоянии операнда. Обещание, созданное и возвращаемое этим вызовом then, относится к расширенному классу и учитывает второй вызов CancellablePromise.prototype.constructor.

Подтверждающие доказательства

  1. new CancellablePromise().constructor is CancellablePromise

class CancellablePromise extends Promise {
  constructor(executor) {
    super(executor);
  }
}

console.log ( new CancellablePromise( ()=>null).constructor.name);
  1. Изменение CancellablePromise.prototype.constructor на Promise для целей тестирования вызывает только один вызов CancellablePromise (потому что await одурачен для возврата своего операнда):

class CancellablePromise extends Promise {
  constructor(executor) {
    console.log("CancellablePromise::constructor");
    super(executor);
  }
}
CancellablePromise.prototype.constructor = Promise; // TESTING ONLY

async function test() {
   await new CancellablePromise( ()=>null);
}
test();


Второе обновление (огромное спасибо ссылкам, предоставленным ОП)

Соответствующие реализации

Согласно спецификации await

await создает анонимное промежуточное Promise обещание с обработчиками onFulilled и onRejected для обоих возобновите выполнение после оператора await или сгенерируйте из него ошибку, в зависимости от того, в каком установленном состоянии выполняется промежуточное обещание.

Он (await) также вызывает then в обещании операнда выполнить или отклонить промежуточное обещание. Этот конкретный вызов then возвращает обещание класса operandPromise.constructor. Хотя возвращаемое обещание then никогда не используется, регистрация в конструкторе расширенного класса выявляет вызов.

Если значение расширенного обещания constructor будет изменено на Promise для экспериментальных целей, вышеуказанный вызов then будет без вывода сообщений вернуть обещание класса Promise.


Приложение: Расшифровка спецификации await

  1. Пусть asyncContext будет текущим контекстом выполнения.

  2. Пусть обещание будет! NewPromiseCapability (% Promise%).

Создает новый jQuery-подобный отложенный объект со свойствами promise, resolve и reject, называя его «Запись PromiseCapability». Объект promise объекта deferred имеет базовый класс Promise .

  1. Выполнить! Call (обещание возможности. [[Resolve]], не определено, «обещание»).

Разрешите отложенное обещание с правильным операндом await. Процесс разрешения либо вызывает метод операнда then, если он является «тогда доступным», либо выполняет отложенное обещание, если операндом является какое-то другое значение, не являющееся обещанием.

  1. Пусть stepsFulfilled - шаги алгоритма, определенные в Await Fulfilled Functions.

  2. Пусть onFulfilled будет функцией CreateBuiltin (stepsFulfilled, «[[AsyncContext]]»).

  3. Установить onFulfilled. [[AsyncContext]] в asyncContext.

Создайте обработчик onfulfilled для возобновления операции await внутри функции async, в которую она была вызвана, возвращая выполненное значение операнда, переданного обработчику в качестве аргумента.

  1. Пусть stepsRejected - шаги алгоритма, определенные в Await Rejected Functions.

  2. Пусть onRejected будет CreateBuiltinFunction (stepsRejected, «[[AsyncContext]]»).

  3. Установить для OnRejected. [[AsyncContext]] значение asyncContext.

Создайте отвергнутый обработчик для возобновления операции await внутри функции async, в которую она была вызвана, путем выдачи причины отклонения обещания, переданной обработчику в качестве аргумента.

  1. Выполнить! PerformPromiseThen (обещание возможности. [[Promise]], onFulfilled, onRejected).

Вызовите then по отложенному обещанию с этими двумя обработчиками, чтобы await мог ответить на установление своего операнда.

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

  1. Удалите asyncContext из стека контекста исполнения и восстановите контекст исполнения, находящийся на вершине стека контекста исполнения, в качестве запущенного контекста исполнения.

  2. Установите состояние оценки кода asyncContext таким образом, чтобы при возобновлении оценки с завершением завершения были выполнены следующие шаги алгоритма, вызвавшего Await, с доступным завершением.

Сохранение места возобновления после успешного выполнения await и возврата к циклу событий или диспетчеру очередей микрозадач.

...