JavaScriptで「グローバル」待機を使用する









JavaScriptの記述方法を変える可能性のある新機能は、現代のWebの進化を形作っている非常に柔軟で強力な言語です。JavaScriptがWeb開発で非常に支配的である主な理由の1つは、その迅速な開発と継続的な改善です。



JavaScriptを改善するための1つの提案は、提案です。「トップレベル待機」と呼ばれます(トップレベル待機、「グローバル」待機)。この提案の目的は、ESモジュールを非同期関数のようなものに変えることです。これにより、モジュールはすぐに使用できるリソースを取得し、モジュールがそれらをインポートするのをブロックできます。待機中のリソースをインポートするモジュールは、リソースが受信されて使用できるように準備された後でのみ、コードの実行を実行できます。



この提案は現在3段階で検討されているため、この機能はまだ本番環境では使用できません。ただし、近い将来、確実に実装されることは間違いありません。



これについては心配しないでください。読み続けます。名前付き機能を今すぐ使用する方法を紹介します。



通常の待機の何が問題になっていますか?



非同期関数の外部でawaitキーワードを使用しようとすると、構文エラーが発生します。これを回避するために、開発者は即時呼び出し関数式(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関数は、遅延後に解決するpromiseを返します。この関数は非同期であるため、IIFE内で「await」キーワードを使用して、完了するのを「待機」します。実際のアプリケーションでは、「遅延」関数の代わりに、フェッチ(データの受信要求)またはその他の非同期タスクの呼び出しがあります。 promiseを解決した後、変数に値を割り当てます。これは、promiseが解決されるまで変数が未定義になることを意味します。



コードの最後で、他のコードで使用できるように変数をエクスポートします。



これらの変数がインポートされて使用されるコードを見てみましょう。



// 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


このコードを実行すると、最初の2つのケースでは未定義になり、3番目と4番目のケースではそれぞれ169と13になります。なぜそれが起こるのですか?



これは、非同期関数を実行する前に、main.jsのmiddleware.jsからエクスポートされた変数の値を取得しようとしているためです。解決待ちの約束があることを覚えていますか?



この問題を解決するには、変数を使用する準備ができていることをインポートモジュールに通知する必要があります。



回避策


この問題を解決するには、少なくとも2つの方法があります。



1.初期化のためのエクスポートの約束


まず、IIFEをエクスポートできます。asyncキーワードはメソッドを非同期にします。そのようなメソッドは、常にpromiseを返します。これが、以下の例で、非同期IIFEがpromiseを返す理由です。



// 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.エクスポートされた変数を使用したIIFEpromiseの解決


この場合、変数を個別にエクスポートする代わりに、非同期IIFEから変数を返します。これにより、「main.js」ファイルは、Promiseが解決してその値を取得するのを待つだけで済みます。



// 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モジュールと比較して、静的分析機能、テスト容易性、人間工学などの点で大幅な回帰を表しています。」



「グローバル」はどのようにこの問題を解決するのを待っていますか?



トップレベルの待機により、モジュラーシステムが約束の解決とそれらの相互作用を処理できます。



// 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


middleware.jsのpromiseが解決されるまで、main.jsのステートメントは実行されません。これは、回避策よりもはるかにクリーンなソリューションです。



ノート


グローバル待機は、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


xとyは別々のモジュールであり、互いに関連していないため、このスニペットは、ご想像のとおり、X1、X2、Yをコンソールに表示しません。問題の機能をよりよく理解するために、提案のFAQセクション



を調べることを強くお勧めします



実装



V8


この機能は今すぐテストできます。



これを行うには、Chromeがマシン上にあるディレクトリに移動します。すべてのブラウザタブが閉じていることを確認してください。ターミナルを開き、次のコマンドを入力します。



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


Node.jsでこの機能を試すこともできます。詳細については、このガイドお読みください



ESモジュール


値が「module」の「script」タグに「type」属性を必ず追加してください。



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


通常のスクリプトとは異なり、ES6モジュールは共有オリジン(シングルソース)(SOP)およびリソース共有(CORS)ポリシーに従うことに注意してください。したがって、サーバー上でそれらを操作することをお勧めします。



ユースケース



提案に よると、「グローバル」の使用例は次のとおりです。



動的依存パス


const strings = await import(`/i18n/${navigator.language}`);


これにより、モジュールはランタイム値を使用して依存関係パスを計算でき、開発/本番コードの分離、国際化、ランタイム(ブラウザ、Node.js)に基づくコードの分離などに役立ちます。



リソースの初期化


const connection = await dbConnector()


これは、モジュールがすぐに使用できるリソースを取得し、モジュールが使用できないときに例外をスローするのに役立ちます。このアプローチは、以下に示すように、セーフティネットとして使用できます。



フォールバックオプション


以下の例は、「グローバル」待機を使用して、フォールバック実装で依存関係をロードする方法を示しています。CDN Aからのインポートが失敗した場合、CDNBからのインポートが実行されます。



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


批判



リッチハリスはトップレベルの待望の批判のリストをまとめましたこれには次のものが含まれます。



  • 「グローバル」待機はコードの実行をブロックできます
  • 「グローバル」待機は、リソースの取得をブロックできます
  • CommonJSモジュールのサポートの欠如


これらのコメントへの回答は、FAQ提案に記載されています。



  • 子ノード(モジュール)には実行機能があるため、最終的にコードブロッキングは発生しません。
  • 「グローバル」待機は、モジュールグラフの実行フェーズで使用されます。この段階では、すべてのリソースが受信およびリンクされているため、リソースの取得がブロックされるリスクはありません。
  • トップレベルの待機はES6モジュールに制限されています。CommonJSモジュールのサポートは、通常のスクリプトのように、当初は計画されていませんでした


繰り返しになりますが、提案のFAQを読むことを強くお勧めします。



問題の提案の本質をわかりやすく説明できたと思います。この機会を利用しますか?コメントであなたの意見を共有してください。



All Articles