August 14, 2023

async/await на самом деле

Давайте решим элементарную задачу. Что выведет следующий код?

  function f() {
    Promise.resolve(1).then(console.log);
    console.log(2);

  }

  f();

А вот этот?

  async function f() {
    await Promise.resolve(1).then(console.log);
    console.log(2);
  }

  f();

Здесь нет никакого подвоха. В первом случае мы получаем 2 и 1, а во втором — наоборот. JS-разработчику это абсолютно понятно: в обычной функции сначала выполняется макротаска, потом микротаска, а в асинхронной, если мы повесили await на какую-либо инструкцию, она выполнится раньше всего, что будет ниже ее.

Мы специально присвоили await промису, чтобы показать, что для await нет исключений и он выполнит любую инструкцию раньше. Но почему так происходит и, главное, как это обрабатывает цикл событий? Мы знаем, что:

  1. запуская макротаску, цикл событий будет ждать ее выполнения и блокировать все остальное (привет, while true!);
  2. запуская микротаску, цикл событий добавляет ее в Microtask Queue и идет по цепочке дальше. Выполнит он ее, когда она вернется с каким-нибудь статусом (fulfilled/rejected).

Ответ прост. Async-функция — это не что иное, как «синтаксический сахар» для генераторов, а await — «сахар» для yield, который возвращает промис.

Разберем подробнее.

Генератор — это функция, способная приостанавливать свое выполнение и возобновлять его позже благодаря встроенному итератору, который может возвращать какие-либо данные по запросу. Вы наверняка это знаете, поэтому мы не будем заострять внимание на разборе определений, но здесь есть любопытная особенность. Код генератора отдает не только значение текущего yield, но и весь код до него или же код между текущим и предыдущим yield. Все, что находится ниже текущего yield, игнорируется до следующего вызова метода next.

  function* f() {
    yield 1;
    console.log('between 1 and 2');
    yield 2;
    console.log('after 2');
    yield 3;

  }

  const gen = f();

  console.log(gen.next()); // { value: 1, done: false }
  console.log(gen.next()); // between 1 and 2 && { value: 2, done: false }
  console.log(gen.next()); // after 2 && { value: 3, done: false }

Перепишем этот код, используя синтаксис async/await:

  async function f() {
    await console.log(1);
    console.log('between 1 and 2');
    await console.log(2);
    console.log('after 2');
    await console.log(3);
  }

  f(); // 1, between 1 and 2, 2, after 2, 3

Основная особенность асинхронной функции здесь в том, что рантайм сам вызывает метод next, когда это необходимо.

Здесь есть важный момент: yield (который await) обязательно возвращает промис, который, в свою очередь, возвращается с каким-либо статусом:

  1. если промис вернулся с fulfilled, рантайм вызывает метод next, и итератор идет к следующему await или же завершает выполнение генератора, если он был единственным/последним;
  2. если промис вернулся с rejected, рантайм выбрасывает ошибку, а весь код ниже игнорируется.

Вернуться в Chulakov Dev