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 нет исключений и он выполнит любую инструкцию раньше. Но почему так происходит и, главное, как это обрабатывает цикл событий? Мы знаем, что:
- запуская макротаску, цикл событий будет ждать ее выполнения и блокировать все остальное (привет, while true!);
- запуская микротаску, цикл событий добавляет ее в 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) обязательно возвращает промис, который, в свою очередь, возвращается с каким-либо статусом: