
Um novo recurso que pode mudar a maneira como escrevemos
JavaScript é uma linguagem altamente flexível e poderosa que está moldando a evolução da web moderna. Um dos principais motivos pelos quais o JavaScript é tão dominante no desenvolvimento da web é seu rápido desenvolvimento e melhoria contínua.
Uma sugestão para melhorar o JavaScript é a sugestãochamado "espera de nível superior" (espera de nível superior, espera "global"). O objetivo desta proposta é transformar os módulos ES em algo como funções assíncronas. Isso permitirá que os módulos obtenham recursos prontos para uso e bloqueiem os módulos que os importam. Os módulos que importam os recursos esperados só poderão executar a execução do código depois que os recursos forem recebidos e preparados para uso.
Esta proposta está atualmente em 3 estágios de consideração, portanto, esse recurso ainda não pode ser usado na produção. No entanto, você pode ter certeza que em um futuro próximo certamente será implementado.
Não se preocupe com isso. Continue lendo. Vou mostrar como você pode usar o recurso nomeado agora.
O que há de errado com a espera normal?
Se você tentar usar a palavra-chave "await" fora de uma função assíncrona, obterá um erro de sintaxe. Para evitar isso, os desenvolvedores usam Immediately Invoked Function Expression (IIFE).
await Promise.resolve(console.log("️")); //
(async () => {
await Promise.resolve(console.log("️"))
})();
O problema especificado e sua solução são apenas a ponta do iceberg.
Ao trabalhar com módulos ES6, você tende a lidar com muitas instâncias exportando e importando valores. Vamos considerar um exemplo:
// 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 };
No exemplo acima, estamos exportando e importando variáveis entre library.js e middleware.js. Você pode nomear os arquivos como quiser.
A função de atraso retorna uma promessa que resolve após um atraso. Como essa função é assíncrona, usamos a palavra-chave "await" dentro do IIFE para "esperar" que ele seja concluído. Em um aplicativo real, em vez da função de "atraso", haverá uma chamada de busca ou alguma outra tarefa assíncrona. Depois de resolver a promessa, atribuímos o valor à nossa variável. Isso significa que nossa variável ficará indefinida até que a promessa seja resolvida.
No final do código, exportamos nossas variáveis para que possam ser usadas em outro código.
Vamos dar uma olhada no código onde essas variáveis são importadas e usadas:
// 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
Se você executar este código, obterá indefinido nos primeiros dois casos e 169 e 13 no terceiro e quarto casos, respectivamente. Por que isso acontece?
Isso se deve ao fato de que estamos tentando obter os valores das variáveis exportadas de middleware.js em main.js antes da execução da função assíncrona. Você se lembra que temos uma promessa pendente de resolução?
Para resolver esse problema, precisamos informar de alguma forma ao módulo de importação que as variáveis estão prontas para uso.
Soluções Alternativas
Existem pelo menos duas maneiras de resolver esse problema.
1. Exportando uma promessa de inicialização
Primeiro, o IIFE pode ser exportado. A palavra-chave async torna o método assíncrono, tal método sempre retorna uma promessa. É por isso que, no exemplo abaixo, o IIFE assíncrono retorna uma promessa.
// 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 };
Ao acessar variáveis exportadas em main.js, você pode aguardar a execução do 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
Apesar do fato de que esse snippet resolve o problema, ele leva a outros problemas.
- Ao usar o modelo especificado, você deve procurar a promessa desejada
- Se outro módulo também usar as variáveis "squareOutput" e "diagonalOutput", devemos garantir que o IIFE seja reexportado
Também existe outra maneira.
2. Resolução da promessa do IIFE com variáveis exportadas
Nesse caso, em vez de exportar as variáveis individualmente, nós as retornamos de nosso IIFE assíncrono. Isso permite que o arquivo "main.js" simplesmente espere pela promessa de resolver e recuperar seu 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
No entanto, essa solução também tem algumas desvantagens.
De acordo com a sugestão, “este padrão tem uma séria desvantagem, pois requer uma refatoração significativa da fonte de recursos associada em modelos mais dinâmicos e colocando a maior parte do corpo do módulo no retorno de chamada .then () para permitir que os módulos dinâmicos sejam usados. Isso representa uma regressão significativa em termos de capacidade de análise estática, testabilidade, ergonomia e muito mais em comparação com os módulos ES2015. "
Como a espera "global" resolve este problema?
Aguardar de nível superior permite que um sistema modular cuide da resolução de promessas e de como elas interagem entre si.
// 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
Nenhuma das instruções em main.js é executada até que as promessas em middleware.js sejam resolvidas. Esta é uma solução muito mais limpa do que as soluções alternativas.
A anotação
Aguardar global funciona apenas com módulos ES. As dependências usadas devem ser especificadas explicitamente. O exemplo abaixo do repositório de propostas demonstra bem isso.
// 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 snippet não exibirá X1, X2, Y no console, como você pode esperar, já que xey são módulos separados, não relacionados entre si.
Recomendo enfaticamente que você estude a seção de perguntas frequentes da proposta para uma melhor compreensão do recurso em questão.
Implementação
V8
Você pode testar esse recurso agora.
Para fazer isso, vá para o diretório onde o Chrome está localizado em sua máquina. Certifique-se de que todas as guias do navegador estejam fechadas. Abra um terminal e digite o seguinte comando:
chrome.exe --js-flags="--harmony-top-level-await"
Você também pode experimentar esse recurso no Node.js. Leia este guia para saber mais.
Módulos ES
Certifique-se de adicionar o atributo "type" à tag "script" com o valor "module".
<script type="module" src="./index.js"></script>
Observe que, ao contrário dos scripts regulares, os módulos ES6 seguem uma política de origem compartilhada (fonte única) (SOP) e compartilhamento de recursos (CORS). Portanto, é melhor trabalhar com eles no servidor.
Casos de uso
De acordo com a proposta, os casos de uso para "global" await são os seguintes:
Caminho de dependência dinâmica
const strings = await import(`/i18n/${navigator.language}`);
Isso permite que os módulos usem valores de tempo de execução para calcular caminhos de dependência e pode ser útil para desacoplar código de desenvolvimento / produção, internacionalização, divisão de código com base no tempo de execução (navegador, Node.js), etc.
Inicializando recursos
const connection = await dbConnector()
Isso ajuda os módulos a obter recursos prontos para uso e lançar exceções quando o módulo não pode ser usado. Essa abordagem pode ser usada como uma rede de segurança, conforme mostrado abaixo.
Opção alternativa
O exemplo abaixo mostra como um await "global" pode ser usado para carregar uma dependência com uma implementação de fallback. Se a importação do CDN A falhar, a importação do CDN B é realizada:
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 compilou uma lista de críticas de alto nível aguardam. Inclui o seguinte:
- "Global" await pode bloquear a execução do código
- Aguardar "global" pode bloquear a aquisição de recursos
- Falta de suporte para módulos CommonJS
As respostas a esses comentários são fornecidas na proposta do FAQ:
- Uma vez que os nós filhos (módulos) têm a capacidade de executar, em última análise, não há bloqueio de código
- O await "global" é usado durante a fase de execução de um gráfico de módulo. Nesta fase, todos os recursos são recebidos e vinculados, portanto, não há risco de bloquear a aquisição de recursos
- A espera de nível superior é limitada a módulos ES6. O suporte para módulos CommonJS, como scripts regulares, não foi planejado originalmente
Mais uma vez, recomendo fortemente a leitura do FAQ da proposta.
Espero ter conseguido explicar a essência da proposta em questão de uma forma acessível. Você vai aproveitar essa oportunidade? Compartilhe sua opinião nos comentários.