Одним из недостатков замечательного подхода JavaScript async и await является то, что вы должны использовать его в функции. Это ограничение, похоже, вот-вот исчезнет, когда вы прочитаете заголовки вроде «Ожидание верхнего уровня, реализованное в V8». Однако вам нужно прочитать мелкий шрифт.
Вы не можете поместить ожидание на верхний уровень «основной» программы JavaScript, потому что код обычно выполняется в синхронном стиле. То есть стандартная программа JavaScript запускает весь код на верхнем уровне, а затем поток переходит в очередь событий и начинает обработку событий. Вы можете быть на 100% уверены, что код верхнего уровня завершится до того, как будут обработаны какие-либо события, а затем выполнение будет полностью асинхронным, переходя к обработчикам событий и обратным вызовам. Если бы вы поместили ожидание в код верхнего уровня, то оценка кода верхнего уровня была бы остановлена, и поток был бы освобожден для обработки очереди событий. Это означало бы, что события начали обрабатываться до того, как завершился код верхнего уровня. По-видимому, это плохая идея, даже если я думаю, что с этим можно будет справиться в новом коде.
Если вы не заметили, код верхнего уровня выполняется до завершения, даже если вы вызываете асинхронную функцию, которая выполняет ожидание. Что происходит, так это то, что функция async выполняется до тех пор, пока не достигнет первого ожидания, когда она приостанавливается и освобождает поток. Поток не уходит в очередь событий; вместо этого он продолжает выполнение инструкции после функции и выполняет код верхнего уровня до завершения. Если бы вы поместили ожидание в код верхнего уровня, он должен был бы приостановиться, и единственное, что мог бы сделать поток, — это запустить четную очередь, но, поместив его в функцию, вы не остановите код верхнего уровня.
Итак, как это соотносится с новостью о том, что теперь в ECMAScript есть ожидание верхнего уровня, реализованное в V8, Babel и Webpack, и скоро оно появится в браузере рядом с вами? Вы действительно можете использовать await в коде верхнего уровня, но только если этот код находится внутри модуля. Вы по-прежнему не можете поместить await в свой «нормальный» код верхнего уровня. Это означает, что если вы не используете модули, вы все равно не можете поместить ожидание в свой код верхнего уровня. Обратите внимание, однако, что загрузка модуля вызывает выполнение его кода верхнего уровня, как если бы он был частью импортирующего его кода верхнего уровня. Код модуля завершается до продолжения импорта кода. Это означает, что модули загружаются один за другим и запускают свой код верхнего уровня один за другим. Это давний принцип, но сейчас все немного по-другому.
Все стало усложняться, когда был введен динамический импорт. Это принесло с собой функцию импорта, которая возвращает обещание, которое разрешает объекты, экспортированные в модуль. Обратите внимание, что это вводит некоторую асинхронность в код верхнего уровня. Вы можете динамически загружать модуль, настраивать метод then для Promise, и код верхнего уровня будет выполняться до завершения, а Promise разрешится позже. Обратите внимание, что обещание всегда разрешается после завершения кода верхнего уровня. Вы также можете использовать динамический импорт в других модулях, и теперь у нас есть ситуация, когда модули могут завершить свой код до того, как модули, которые они импортируют, завершились.
Теперь вы можете использовать Promise для асинхронной загрузки модулей, но вы по-прежнему не можете ожидать модуль. Причина по-прежнему в том, что ожидание в коде верхнего уровня будет означать, что он должен ждать и не завершится до обработки очереди событий. Однако такой проблемы нет, если коду верхнего уровня в модуле разрешено ожидать Promise. В этом случае поток будет освобожден для продолжения выполнения кода верхнего уровня, который импортировал модуль, и, поскольку это будет возвращаться к вложению импорта, код верхнего уровня будет завершен, потому что он все еще не может иметь ожидание внутри него. Вся эта идея теперь применима как к статическому импорту, так и к динамическому. Это как если бы статический импорт также создал обещание.
Это означает, что порядок выполнения кода модуля верхнего уровня больше не является на 100% синхронным. Каждый импортированный модуль, статический или динамический, будет выполняться до тех пор, пока не достигнет ожидания, а затем остановится, освобождая поток для продолжения в коде, который его импортировал.
Рассмотрим следующий пример из предложения:
// x.mjsconsole.log («X1»); ожидание нового обещания (r => setTimeout (r, 1000)); console.log («X2»);
// y.mjsconsole.log («Y»);
// z.mjsimport «./x.mjs»;import» ./y.mjs «;
Здесь у нас есть два модуля x и y, которые что-то печатают в журнале. Без await в модуле x вы ожидали бы, что модуль x завершится и напечатает X1, а через секунду — X2, а затем y будет импортирован, и он напечатает Y. С await происходит другое. Модуль x печатает X1, затем выполняет ожидание и освобождает поток, который запускает импорт модуля y, который затем печатает Y. Через одну секунду и после завершения всего кода верхнего уровня Promise разрешается, и поток продолжает модуль x после ожидание и так печатает X2.
Без await мы видим X1, X2, Y, а с await мы видим X1, Y, X2. Мы явно должны быть осторожны с порядком, в котором что-то происходит, но заметим, что здесь нет состояния гонки — с ожиданием все всегда происходит в одном и том же порядке. Также обратите внимание, что ожидание находится в коде верхнего уровня в модуле, а не в скрипте.
Все это означает, что часто обсуждаемое ожидание верхнего уровня на самом деле не является ожиданием верхнего уровня, которого мы все, возможно, ожидали. В обозримом будущем нам все равно придется помещать ожидание в функции — даже если они будут немедленно выполнены. И снова причина в том, что вы не хотите останавливать код верхнего уровня с помощью await — он должен выполняться до завершения, чтобы запустить цикл обработки событий. Это также гарантирует, что ожидание перезапустится только после завершения всего кода верхнего уровня.
Есть несколько хорошо известных критических замечаний по поводу наличия ожидания верхнего уровня, которое останавливает код верхнего уровня, но если мы изменим правила так, чтобы ожидание в коде верхнего уровня освободило поток для обработки очереди событий, я могу Не вижу, что есть проблема, хотя это непросто.
Ян Эллиот — автор JavaScript Async, в котором он всесторонне рассматривает все асинхронные возможности Javascript, включая Promises и await / async. Его другие названия включают Just JavaScript: Idiomatic Approach и Just jQuery: Events, Async & AJAX.