Почему вам следует использовать  Top-level Await в JavaScript?

JavaScript — очень гибкий и мощный язык, который определяет развитие современного веба. Одна из основных причин главенства JavaScript в сфере веб-разработки заключается в его стремительном развитии и постоянном совершенствовании.

Почему вам следует использовать  Top-level Await в JavaScript?
Почему вам следует использовать  Top-level Await в JavaScript?

Одним из предложений по улучшению JavaScript является предложение под названием «top-level await» (await верхнего уровня, «глобальный» await). Цель данного предложения состоит в превращении ES модулей в некое подобие асинхронных функций. Это позволит модулям получать готовые к использованию ресурсы и блокировать модули, импортирующие их. Модули, которые импортируют ожидаемые ресурсы, смогут запускать выполнение кода только после получения ресурсов и их предварительной подготовки к использованию.

Указанное предложение в настоящее время находится на 3 стадии рассмотрения, поэтому данную возможность пока нельзя использовать в продакшне. Однако можно быть уверенным в том, что в ближайшем будущем она непременно будет реализована.

Не переживайте из-за этого. Продолжайте читать. Я покажу, как можно использовать названную фичу уже сейчас.

Что не так с обычным await?

Если вы попытаетесь использовать ключевое слово «await» за пределами асинхронной функции, то получите синтаксическую ошибку. Во избежание этого разработчики используют немедленно вызываемые функциональные выражения (Immediately Invoked Function Expression, IIFE).

await Promise.resolve(console.log("️")); // Ошибка

(async () => {
    await Promise.resolve(console.log("️"))
})();
Указанная проблема и ее решение — это лишь вершина айсберга

При работе с ES6 модулями вы, как правило, имеете дело с большим количеством экземпляров, экспортирующих и импортирующих значения. Рассмотрим пример:

// library.js
export const sqrt = Math.sqrt;
export const square = (x) => x * x;
export const diagonal = (x, y) => sqrt((square(x) + square(y)));

// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// IIFE
(async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
})();

export { squareOutput, diagonalOutput };

В приведенном примере мы экспортируем и импортируем переменные между library.js и middleware.js. Вы можете назвать файлы как угодно.

Функция «delay» возвращает промис, разрешающийся после задержки. Поскольку данная функция является асинхронной, мы используем ключевое слово «await» внутри IIFE для «ожидания» ее завершения. В реальном приложении вместо функции «delay» будет вызов fetch (запроса на получение данных) или другая асинхронная задача. После разрешения промиса, мы присваиваем значение нашей переменной. Это означает, что до разрешения промиса наша переменная будет иметь значение undefined.

В конце кода мы экспортируем наши переменные для того, чтобы они могли использоваться в другом коде.

Давайте взглянем на код, в котором эти переменные импортируются и используются:

// main.js
import { squareOutput, diagonalOutput } from "./middleware.js";

console.log(squareOutput); // undefined
console.log(diagonalOutput); // undefined
console.log("From Main");

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13

Если вы запустите этот код, то в первых двух случаях получите undefined, а в третьем и четвертом 169 и 13, соответственно. Почему так происходит?

Это происходит из-за того, что мы пытаемся получить значения переменных, экспортируемых из middleware.js, в main.js до завершения выполнения асинхронной функции. Вы помните, что у нас там промис, ожидающий разрешения?

Для решения данной проблемы, нам нужно каким-то образом сообщить импортирующему модулю о готовности переменных к использованию.

Обходные пути

Существует, как минимум, два способа решить обозначенную проблему.

1. Экспорт промиса для инициализации

Во-первых, можно экспортировать IIFE. Ключевое слово «async» делает метод асинхронным, такой метод всегда возвращает промис. Вот почему в приведенном ниже примере асинхронное IIFE возвращает промис.

// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// обходной маневр или, как еще говорят, костыль
export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
})();

export { squareOutput, diagonalOutput };

При получении доступа к экспортируемым переменным в main.js можно подождать выполнения IIFE.

// main.js
import promise, { squareOutput, diagonalOutput } from "./middleware.js";

promise.then(() => {
    console.log(squareOutput); // 169
    console.log(diagonalOutput); // 169
    console.log("From Main");
});

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13

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

  • При использовании указанного шаблона приходится искать нужный промис
  • Если в другом модуле также используются переменные «squareOutput» и «diagonalOutput», мы должны обеспечить реэкспорт IIFE

Существует и другой способ.

2. Разрешение промиса IIFE с экспортируемыми переменными

В данном случае, вместо экспорта переменных по-отдельности, мы возвращаем их из нашего асинхронного IIFE. Это позволяет файлу «main.js» просто ждать разрешения промиса и извлекать его значение.

// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// обходной маневр
export default (async () => {
    await delay(1000);
    squareOutput = square(13);
    diagonalOutput = diagonal(12, 5);
    return { squareOutput, diagonalOutput };
})();

// main.js
import promise from "./middleware.js";

promise.then(({ squareOutput, diagonalOutput }) => {
    console.log(squareOutput); // 169
    console.log(diagonalOutput); // 169
    console.log("From Main");
});

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13

Однако у такого решения также имеются некоторые недостатки.

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

Как «глобальный» await решает данную проблему?

await верхнего уровня позволяет модульной системе заботиться о разрешении промисов и их взаимодействии между собой.

// middleware.js
import { square, diagonal } from "./library.js";

console.log("From Middleware");

let squareOutput;
let diagonalOutput;

const delay = (ms) => new Promise((resolve) => {
    const timer = setTimeout(() => {
        resolve(console.log("️"));
        clearTimeout(timer);
    }, ms);
});

// "глобальный" await
await delay(1000);
squareOutput = square(13);
diagonalOutput = diagonal(12, 5);

export { squareOutput, diagonalOutput };

// main.js
import { squareOutput, diagonalOutput } from "./middleware.js";

console.log(squareOutput); // 169
console.log(diagonalOutput); // 13
console.log("From Main");

const timer1 = setTimeout(() => {
    console.log(squareOutput);
    clearTimeout(timer1);
}, 2000); // 169

const timer2 = setTimeout(() => {
    console.log(diagonalOutput);
    clearTimeout(timer2);
}, 2000); // 13

Ни одна из инструкций в main.js не выполняется до разрешения промисов в middleware.js. Это гораздо более чистое решение по сравнению с обходными путями.

Заметка

Глобальный await работает только с ES модулями. Используемые зависимости должны быть указаны явно. Приведенный ниже пример из репозитория предложения хорошо это демонстирует.

// x.mjs
console.log("X1");
await new Promise(r => setTimeout(r, 1000));
console.log("X2");
// y.mjs
console.log("Y");
// z.mjs
import "./x.mjs";
import "./y.mjs";
// X1
// Y
// X2

Данный сниппет не выведет в консоль X1, X2, Y, как можно ожидать, поскольку x и y — отдельные модули, не связанные между собой.

Я настоятельно рекомендую вам изучить рездел «часто задаваемые вопросы» предложения для лучшего пониманию рассматриваемой фичи.

Реализация

V8

Вы можете протестировать данную возможность уже сейчас.

Для этого перейдите в директорию, в которой размещается Chrome на вашей машине. Убедитесь в том, что все вкладки браузера закрыты. Откройте терминал и введите следующую команду:

chrome.exe --js-flags="--harmony-top-level-await"

Вы таже можете попробовать данную фичу в Node.js. Прочитайте это руководство, чтобы узнать больше.

ES модули

Убедитесь, что добавили тегу «script» атрибут «type» со значением «module».

<script type="module" src="./index.js"></script>

Обратите внимание, что в отличие от обычных скриптов, ES6 модули следуют политике общего происхождения (одного источника) (SOP) и совместного использования ресурсов (CORS). Поэтому с ними лучше работать на сервере.

Случаи использования

Согласно предложению случаями использования «глобального» await является следующее:

Динамический путь зависимости
const strings = await import(`/i18n/${navigator.language}`);

Это позволяет модулям использовать значения среды выполнения для вычисления путей зависимостей и может быть полезным для разделения разработка/продакшн код, интернационализации, разделения кода в зависимости от среды выполнения (браузер, Node.js) и т.д.

Инициализация ресурсов
const connection = await dbConnector()

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

Запасной вариант

В приведенном ниже примере показано, как «глобальный» await может использоваться для загрузки зависимости с реализацией запасного варианта. Если импорт из CDN A провалился, осуществляется импорт из CDN B:

let jQuery;
try {
  jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
  jQuery = await import('https://cdn-b.example.com/jQuery');
}

 

Критика

Rich Harris составил список критических замечаний относительно await верхнего уровня. Он включает в себя следующее:

  • «Глобальный» await может блокировать выполнение кода
  • «Глобальный» await может блокировать получение ресурсов
  • Отсутствует поддержка CommonJS модулей

Вот какие ответы на эти замечания даются в FAQ предложения:

  • Поскольку дочерние узлы (модули) имеют возможность выполнения, блокировка кода, в конечном счете, отсутствует
  • «Глобальный» await используется на стадии выполнения графа модулей. На данном этапе все ресурсы получены и связаны, поэтому риска блокировки получения ресурсов не усматривается
  • await верхнего уровня ограничен ES6 модулями. Поддержка CommonJS модулей, как и обычных скриптов, изначально не планировалась

Я снова настоятельно рекомендую ознакомиться с FAQ предложения.

Надеюсь, мне удалось доступно объяснить суть рассматриваемого предложения. Собираетесь ли использовать эту возможность? Делитесь своим мнением в комментариях.

источник