Una nueva característica que podría cambiar la forma en que escribimos
JavaScript es un lenguaje altamente flexible y poderoso que está dando forma a la evolución de la web moderna. Una de las principales razones por las que JavaScript es tan dominante en el desarrollo web es su rápido desarrollo y mejora continua.
Una sugerencia para mejorar JavaScript es la sugerenciallamado "espera de nivel superior" (espera de nivel superior, espera "global"). El objetivo de esta propuesta es convertir los módulos ES en algo así como funciones asincrónicas. Esto permitirá a los módulos obtener recursos listos para usar y bloquear módulos para importarlos. Los módulos que importan los recursos esperados solo podrán ejecutar la ejecución de código después de que los recursos se hayan recibido y preparado para su uso.
Esta propuesta se encuentra actualmente en 3 etapas de consideración, por lo que esta característica aún no se puede utilizar en producción. Sin embargo, puede estar seguro de que se implementará en un futuro próximo.
No se preocupe por esto. Sigue leyendo. Le mostraré cómo puede usar la función nombrada ahora mismo.
¿Qué pasa con la espera normal?
Si intenta utilizar la palabra clave await fuera de una función asincrónica, obtendrá un error de sintaxis. Para evitar esto, los desarrolladores usan la expresión de función inmediatamente invocada (IIFE).
await Promise.resolve(console.log("️")); //
(async () => {
await Promise.resolve(console.log("️"))
})();
El problema especificado y su solución son solo la punta del iceberg.
Cuando trabaja con módulos ES6, tiende a lidiar con muchas instancias exportando e importando valores. Consideremos un ejemplo:
// 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 };
En el ejemplo anterior, estamos exportando e importando variables entre library.js y middleware.js. Puede nombrar los archivos como desee.
La función de retraso devuelve una promesa que se resuelve después de un retraso. Dado que esta función es asincrónica, usamos la palabra clave "await" dentro del IIFE para "esperar" a que se complete. En una aplicación real, en lugar de la función de "retraso", habría una llamada de recuperación o alguna otra tarea asincrónica. Después de resolver la promesa, asignamos el valor a nuestra variable. Esto significa que nuestra variable no estará definida hasta que se resuelva la promesa.
Al final del código, exportamos nuestras variables para que puedan usarse en otro código.
Echemos un vistazo al código donde se importan y utilizan estas variables:
// 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 ejecuta este código, obtendrá indefinido en los dos primeros casos, y 169 y 13 en el tercer y cuarto casos, respectivamente. ¿Por qué sucede?
Esto se debe al hecho de que estamos intentando obtener los valores de las variables exportadas desde middleware.js en main.js antes de la ejecución de la función asincrónica. ¿Recuerda que tenemos una promesa pendiente de resolución?
Para resolver este problema, necesitamos informar de alguna manera al módulo de importación que las variables están listas para usarse.
Soluciones alternativas
Hay al menos dos formas de resolver este problema.
1. Exportar promesa para inicialización
Primero, el IIFE se puede exportar. La palabra clave async hace que el método sea asincrónico, tal método siempre devuelve una promesa. Por eso, en el ejemplo siguiente, el IIFE asincrónico devuelve una promesa.
// 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 };
Mientras accede a las variables exportadas en main.js, puede esperar a que se ejecute 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
A pesar de que este fragmento resuelve el problema, genera otros problemas.
- Al usar la plantilla especificada, debe buscar la promesa deseada
- Si otro módulo también usa las variables "squareOutput" y "diagonalOutput", debemos asegurarnos de que el IIFE se reexporta
También hay otra forma.
2. Resolución de la promesa IIFE con variables exportadas
En este caso, en lugar de exportar las variables individualmente, las devolvemos desde nuestro IIFE asincrónico. Esto permite que el archivo "main.js" simplemente espere a que la promesa se resuelva y recupere su valor.
// 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
Sin embargo, esta solución también tiene algunas desventajas.
Según la sugerencia, “este patrón tiene un serio inconveniente ya que requiere una refactorización sustancial de la fuente de recursos asociada en plantillas más dinámicas y colocar la mayor parte del cuerpo del módulo en la devolución de llamada .then () para permitir módulos dinámicos. Esto representa una regresión significativa en términos de capacidad de análisis estático, capacidad de prueba, ergonomía y más en comparación con los módulos ES2015 ".
¿Cómo la espera "global" resuelve este problema?
La espera de nivel superior permite que un sistema modular se encargue de resolver las promesas y cómo interactúan entre sí.
// 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
Ninguna de las declaraciones en main.js se ejecuta hasta que se resuelven las promesas en middleware.js. Esta es una solución mucho más limpia que las soluciones alternativas.
La nota
La espera global solo funciona con módulos ES. Las dependencias utilizadas deben especificarse explícitamente. El siguiente ejemplo del repositorio de propuestas demuestra esto 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
Este fragmento no mostrará X1, X2, Y en la consola, como era de esperar, ya que xey son módulos separados, no relacionados entre sí.
Le recomiendo que estudie la sección de preguntas frecuentes de la propuesta para comprender mejor la función en cuestión.
Implementación
V8
Puede probar esta función ahora mismo.
Para hacer esto, vaya al directorio donde se encuentra Chrome en su máquina. Asegúrese de que todas las pestañas del navegador estén cerradas. Abra una terminal e ingrese el siguiente comando:
chrome.exe --js-flags="--harmony-top-level-await"
También puede probar esta función en Node.js. Lea esta guía para obtener más información.
Módulos ES
Asegúrese de agregar el atributo "tipo" a la etiqueta "script" con el valor "módulo".
<script type="module" src="./index.js"></script>
Tenga en cuenta que, a diferencia de los scripts normales, los módulos de ES6 siguen una política de origen compartido (fuente única) (SOP) y uso compartido de recursos (CORS). Por lo tanto, es mejor trabajar con ellos en el servidor.
Casos de uso
Como se sugirió, los casos de uso de la espera "global" son los siguientes:
Ruta de dependencia dinámica
const strings = await import(`/i18n/${navigator.language}`);
Esto permite que los módulos utilicen valores de tiempo de ejecución para calcular rutas de dependencia y puede ser útil para desacoplar código de desarrollo / producción, internacionalización, dividir código según el tiempo de ejecución (navegador, Node.js), etc.
Inicializando recursos
const connection = await dbConnector()
Esto ayuda a los módulos a obtener recursos listos para usar y generar excepciones cuando el módulo no se puede usar. Este enfoque se puede utilizar como red de seguridad, como se muestra a continuación.
Opción de reserva
El siguiente ejemplo muestra cómo se puede utilizar una espera "global" para cargar una dependencia con una implementación de reserva. Si la importación desde CDN A falla, se realiza la importación desde CDN B:
let jQuery;
try {
jQuery = await import('https://cdn-a.example.com/jQuery');
} catch {
jQuery = await import('https://cdn-b.example.com/jQuery');
}
Crítica
Rich Harris ha compilado una lista de críticas de alto nivel en espera. Incluye lo siguiente:
- La espera "global" puede bloquear la ejecución del código
- La espera "global" puede bloquear la adquisición de recursos
- Falta de soporte para módulos CommonJS
Las respuestas a estos comentarios se dan en la propuesta de preguntas frecuentes:
- Dado que los nodos secundarios (módulos) tienen la capacidad de ejecutarse, en última instancia, no hay bloqueo de código
- La espera "global" se utiliza durante la fase de ejecución de un gráfico de módulo. En esta etapa, todos los recursos se reciben y se vinculan, por lo que no hay riesgo de bloquear la adquisición de recursos.
- La espera de nivel superior se limita a los módulos ES6. El soporte para módulos CommonJS, como scripts regulares, no estaba planeado originalmente
Una vez más, recomiendo leer las preguntas frecuentes de la propuesta.
Espero haber podido explicar la esencia de la propuesta en cuestión de forma accesible. ¿Vas a aprovechar esta oportunidad? Comparte tu opinión en los comentarios.