Utilisation de l'attente "globale" en JavaScript





Une nouvelle fonctionnalité qui pourrait changer la façon dont nous écrivons



JavaScript est un langage très flexible et puissant qui façonne l'évolution du Web moderne. L'une des principales raisons pour lesquelles JavaScript est si dominant dans le développement Web est son développement rapide et son amélioration continue.



Une suggestion pour améliorer JavaScript est la suggestionappelé "attente de niveau supérieur" (attente de niveau supérieur, attente "globale"). Le but de cette proposition est de transformer les modules ES en quelque chose comme des fonctions asynchrones. Cela permettra aux modules d'obtenir des ressources prêtes à l'emploi et de bloquer les modules en les important. Les modules qui importent des ressources en attente ne pourront exécuter l'exécution de code qu'une fois les ressources reçues et préparées pour l'utilisation.



Cette proposition est actuellement à 3 étapes de considération, donc cette fonctionnalité ne peut pas encore être utilisée en production. Cependant, vous pouvez être sûr qu'il sera certainement mis en œuvre dans un proche avenir.



Ne t'inquiète pas pour ça. Continue de lire. Je vais vous montrer comment vous pouvez utiliser la fonction nommée dès maintenant.



Quel est le problème avec l'attente normale?



Si vous essayez d'utiliser le mot clé "await" en dehors d'une fonction asynchrone, vous obtiendrez une erreur de syntaxe. Pour éviter cela, les développeurs utilisent l'expression de fonction immédiatement appelée (IIFE).



await Promise.resolve(console.log("️")); // 

(async () => {
    await Promise.resolve(console.log("️"))
})();


Le problème spécifié et sa solution ne sont que la pointe de l'iceberg.




Lorsque vous travaillez avec des modules ES6, vous avez tendance à traiter de nombreuses instances qui exportent et importent des valeurs. Prenons un exemple:



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


Dans l'exemple ci-dessus, nous exportons et importons des variables entre library.js et middleware.js. Vous pouvez nommer les fichiers comme vous le souhaitez.



La fonction delay renvoie une promesse qui se résout après un délai. Puisque cette fonction est asynchrone, nous utilisons le mot-clé "await" à l'intérieur de l'IIFE pour "attendre" qu'elle se termine. Dans une application réelle, au lieu de la fonction "delay", il y aura un appel de récupération ou une autre tâche asynchrone. Après avoir résolu la promesse, nous attribuons la valeur à notre variable. Cela signifie que notre variable ne sera pas définie jusqu'à ce que la promesse soit résolue.



À la fin du code, nous exportons nos variables afin qu'elles puissent être utilisées dans un autre code.



Jetons un œil au code où ces variables sont importées et utilisées:



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


Si vous exécutez ce code, vous obtiendrez undefined dans les deux premiers cas, et 169 et 13 dans les troisième et quatrième cas, respectivement. Pourquoi ça arrive?



Cela se produit parce que nous essayons d'obtenir les valeurs des variables exportées depuis middleware.js dans main.js avant la fin de l'exécution de la fonction asynchrone. Vous souvenez-vous que nous avons une promesse en attente de résolution?



Pour résoudre ce problème, nous devons en quelque sorte informer le module d'importation que les variables sont prêtes à être utilisées.



Solutions de contournement


Il existe au moins deux façons de résoudre ce problème.



1. Exporter la promesse pour l'initialisation


Premièrement, l'IIFE peut être exporté. Le mot clé async rend la méthode asynchrone, une telle méthode retourne toujours une promesse. C'est pourquoi, dans l'exemple ci-dessous, l'IIFE asynchrone renvoie une promesse.



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


Lors de l'accès aux variables exportées dans main.js, vous pouvez attendre l'exécution de l'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


Malgré le fait que cet extrait résout le problème, cela entraîne d'autres problèmes.



  • Lorsque vous utilisez le modèle spécifié, vous devez trouver la bonne promesse
  • Si un autre module utilise également les variables "squareOutput" et "diagonalOutput", il faut s'assurer que l'IIFE est réexporté


Il existe également un autre moyen.



2. Résolution de la promesse IIFE avec les variables exportées


Dans ce cas, au lieu d'exporter les variables individuellement, nous les renvoyons de notre IIFE asynchrone. Cela permet au fichier "main.js" d'attendre simplement que la promesse soit résolue et de récupérer sa valeur.



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


Cependant, cette solution présente également certains inconvénients.



Selon la suggestion, «ce modèle a un sérieux inconvénient en ce qu'il nécessite une refactorisation substantielle de la source de ressources associée en modèles plus dynamiques, et en plaçant la majeure partie du corps du module dans le rappel .then () pour permettre des modules dynamiques. Cela représente une régression significative en termes de capacité d'analyse statique, de testabilité, d'ergonomie et plus par rapport aux modules ES2015. "



Comment l'attente «globale» résout-elle ce problème?



l'attente de haut niveau permet à un système modulaire de prendre en charge la résolution des promesses et la façon dont elles interagissent les unes avec les autres.



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


Aucune des instructions de main.js n'est exécutée tant que les promesses de middleware.js ne sont pas résolues. C'est une solution beaucoup plus propre que les solutions de contournement.



La note


L'attente globale ne fonctionne qu'avec les modules ES. Les dépendances utilisées doivent être spécifiées explicitement. L'exemple ci-dessous du référentiel de propositions le montre bien.



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


Cet extrait de code n'affichera pas X1, X2, Y sur la console, comme vous pouvez vous y attendre, car x et y sont des modules séparés, non liés les uns aux autres.



Je vous recommande vivement d'étudier la section FAQ de la proposition pour une meilleure compréhension de la fonctionnalité en question.



la mise en oeuvre



V8


Vous pouvez tester cette fonctionnalité dès maintenant.



Pour ce faire, accédez au répertoire où se trouve Chrome sur votre machine. Assurez-vous que tous les onglets du navigateur sont fermés. Ouvrez un terminal et entrez la commande suivante:



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


Vous pouvez également essayer cette fonctionnalité dans Node.js. Lisez ce guide pour en savoir plus.



Modules ES


Assurez-vous d'ajouter l'attribut "type" à la balise "script" avec la valeur "module".



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


Veuillez noter que contrairement aux scripts classiques, les modules ES6 suivent une politique d'origine partagée (source unique) (SOP) et de partage de ressources (CORS). Par conséquent, il est préférable de travailler avec eux sur le serveur.



Cas d'utilisation



Selon la proposition, les cas d'utilisation de l'attente "globale" sont les suivants:



Chemin de dépendance dynamique


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


Cela permet aux modules d'utiliser des valeurs d'exécution pour calculer les chemins de dépendances et peut être utile pour découpler le code de développement / production, l'internationalisation, le découplage du code basé sur le runtime (navigateur, Node.js), etc.



Initialisation des ressources


const connection = await dbConnector()


Cela aide les modules à obtenir des ressources prêtes à l'emploi et à lever des exceptions lorsque le module ne peut pas être utilisé. Cette approche peut être utilisée comme filet de sécurité, comme indiqué ci-dessous.



Option de secours


L'exemple ci-dessous montre comment une attente "globale" peut être utilisée pour charger une dépendance avec une implémentation de secours. Si l'importation depuis CDN A échoue, l'importation depuis CDN B est effectuée:



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


Critique



Rich Harris a compilé une liste de critiques de l' attente de haut niveau. Il comprend les éléments suivants:



  • L'attente "globale" peut bloquer l'exécution du code
  • L'attente "globale" peut bloquer l'acquisition de ressources
  • Manque de prise en charge des modules CommonJS


Les réponses à ces commentaires sont données dans la proposition de FAQ:



  • Étant donné que les nœuds enfants (modules) ont la capacité de s'exécuter, il n'y a finalement aucun blocage de code
  • L'attente "globale" est utilisée lors de la phase d'exécution d'un graphe de module. À ce stade, toutes les ressources sont reçues et liées, il n'y a donc aucun risque de bloquer l'acquisition de ressources
  • L'attente de niveau supérieur est limitée aux modules ES6. La prise en charge des modules CommonJS, comme les scripts standards, n'était pas initialement prévue


Encore une fois, je recommande vivement de lire la FAQ de la proposition.



J'espère avoir été en mesure d'expliquer l'essence de la proposition en question d'une manière accessible. Allez-vous profiter de cette opportunité? Partagez votre opinion dans les commentaires.



All Articles