Async/Await explicado por meio de diagramas e exemplos

Introdução


A sintaxe assíncrono / aguardar em JavaScript ES7 facilita a coordenação de promessas. Se você tiver dados para buscar em diferentes bancos de dados ou APIs, de forma assíncrona e em uma determinada ordem, poderá acabar com um espaguete de promessas e retornos de chamada. A estrutura assíncrono / aguardar torna possível descrever sucintamente essa lógica com um código mais legível e fácil de manter.
Este artigo explica a sintaxe e a semântica do JavaScript async/await com diagramas e exemplos simples.
Mas primeiro, vamos começar com uma breve visão geral das promessas. Sinta-se à vontade para pular esta seção se estiver familiarizado com promessas em JS.

Resumo

promessas

Em JavaScript, uma promessa representa uma abstração de execução assíncrona sem bloqueio. As promessas JS são semelhantes a Java Futuros ou tarefa C#, se você já os encontrou.
As promessas são normalmente usadas para operações de rede e de E/S: por exemplo, ler um arquivo ou fazer uma chamada HTTP. Em vez de bloquear o fio runtime atual, criamos uma promessa assíncrona e usamos o método then para anexar um ligue de volta que será ativado quando o compromisso for concluído. O ligue de volta pode retornar uma promessa, e assim podemos encadear promessas de forma eficiente.
Para simplificar todos os exemplos, vamos supor que a biblioteca request-promise já foi instalado e carregado como:

const rp = require('request-promise');

Agora podemos fazer uma requisição HTTP GET que retorna uma promessa como esta:

const promessa = rp('http://example.com/');

A seguir, vejamos este exemplo:

console.log('Iniciando Execução'); const promessa = rq('http://example.com/'); promessa. then(resultado => console. log(resultado)); console.log("Não é possível saber se a promessa já terminou...");

Criamos uma nova promessa na linha 3 e anexamos um ligue de volta na linha 4. A promessa é assíncrona, portanto, quando chegamos à linha 6, não podemos dizer se a promessa está completa. Se executarmos o código várias vezes, os resultados poderão ser diferentes a cada vez.
De maneira mais geral, o código após qualquer promessa criada é executado simultaneamente com a promessa.
Não é possível bloquear a sequência de operação até que a promessa seja concluída. O que difere de Java Future.get, que bloqueia a fio funcionando até que um Future seja finalizado. Em JavaScript, você não pode esperar facilmente uma promessa.
O diagrama a seguir descreve o comportamento da amostra.
Diagrama do 1º exemplo
O processo de uma promessa.
A única maneira de programar código após uma promessa é especificar um ligue de volta através do método then.
La ligue de volta anexado via then só é executado após a conclusão da promessa. Se falhar (por exemplo, devido a um erro de rede), seu ligue de volta não será executado em mim. Para lidar com promessas falhadas, pode-se anexar outro callback via catch:

rp('http://example.com/'); .then(()=> console.log('Sucesso')) .catch(e =>console.log(`Falha. ${e}`));

Finalmente, para os testes, pode-se facilmente criar promessas “fictícias” que terminam sem/com erro, respectivamente, através dos métodos Promise.resolve et Promise.reject.

const sucesso = Promessa. resolve('Resolvido'); // Imprime "Resultado bem sucedido: Resolvido" success .then(result => console.log(`Resultado bem sucedido: ${result}`)) .catch(e => console.log(`Falha com: ${e}` )); const fail = Promise.Rejeite('Err'); //Impressora "Falha com: Err" success .then(result => console.log(`Resultado bem sucedido: ${result}`)) .catch(e => console.log(`Falha com: ${e}` ));

Para um tutorial mais detalhado sobre promises, convido você a ler este artigo.

O problema – Compondo promessas

Usar uma única promessa é bastante simples. No entanto, quando é necessário codificar uma lógica assíncrona complexa, pode-se combinar com sucesso várias promessas.
Escreva todas as cláusulas then e retornos de chamada assíncronos podem degenerar rapidamente.
Por exemplo, imagine que precisamos codificar um programa que:

  1. Faz uma chamada HTTP, aguarda a conclusão e imprime o resultado;
  2. E faça mais duas chamadas HTTP em paralelo;
  3. Assim que ambas as chamadas forem concluídas, imprima seu resultado.

O trecho de código a seguir mostra uma maneira de fazer isso:

// Faça a primeira chamada const call1promise = rp('http://example.com/'); call1promise.then( result => { // Feito após a primeira chamada terminar console.log(result); const call2promise = rp('http://example.com/'); const call3promise = rp('http: //example .com/'); return Promise.all([call2promise,call3promise]); } ).then( arr =>{ // Feito após a conclusão de ambas as chamadas console.log(arr[0]); console.log(arr[ 1]); });

Começamos fazendo a primeira chamada HTTP e programamos um retorno de chamada para processar o resultado da promessa.
No retorno de chamada, criamos duas outras promessas para futuras solicitações HTTP.
Essas duas promessas são concorrentes e exigem o estabelecimento de um ligue de volta quando ambos terminam. Devemos então combiná-los em uma única promessa via Promise.all, que cria uma promessa que termina assim que todas as promessas simultâneas são concluídas.
O resultado do primeiro ligue de volta é uma promessa, então temos que encadeá-los novamente com um ligue de volta then que exibirá os resultados.
O diagrama a seguir mostra o fluxo de execução:
Exemplo de diagrama 2
Fluxo de execução da combinação de promessas.
Para um exemplo tão simples, terminamos com dois ligue de volta then e devemos usar Promise.all para sincronizar promessas concorrentes.

Mas e se tivéssemos mais operações assíncronas ou se tivéssemos que adicionar tratamento de erros?

Com essa abordagem, pode-se facilmente acabar com um código de espaguete consistindo em then, Promise.all e retorno de chamada.

Função assíncrona

Uma função assíncrona é um atalho para definir uma função que retorna uma promessa.
Por exemplo, as seguintes definições são equivalentes:

function f(){ return Promessa. resolve('TEST'); } //asyncF é equivalente a f! função assíncrona asyncF(){ return 'TESTE'; }

Da mesma forma, funções assíncronas que lançam exceções são equivalentes a funções que retornam promessas rejeitadas:

function f(){ return Promessa. rejeitar('Erro'); } //asyncF é equivalente a f função assíncrona asyncF(){ return 'Error'; }

Aguardam

Quando criamos uma promessa, só podemos esperar que ela seja concluída de forma assíncrona com um retorno de chamada via then. De fato, esperar por uma promessa não é possível para encorajar o desenvolvimento de código não bloqueante.
Caso contrário, os desenvolvedores ficariam tentados a realizar operações de bloqueio porque é mais fácil do que trabalhar com promessas e ligue de volta.
No entanto, para sincronizar promessas, precisamos ser capazes de esperar. Em outras palavras, se uma operação é assíncrona (ou seja, envolta em uma promessa), ela deve poder aguardar a conclusão de outra operação assíncrona.

Mas como o interpretador JavaScript poderia saber se uma operação está sendo executada em uma promessa ou não?

A resposta está no termo async. O interpretador JavaScript sabe que todas as operações em funções async será encapsulado em promessas e será assíncrono. Portanto, é possível permitir que outras promessas aguardem a conclusão.
Vamos descobrir agora await : Só pode ser usado com funções async, e permite esperar de forma síncrona por uma promessa. Se usarmos uma promessa fora de uma função async devemos usar o ligue de volta then.

async function f(){ // resposta será avaliada como o valor resolvido da promessa const response = await rp('http://example.com/'); console.log(resposta); } // Não podemos usar await fora da função assíncrona. // Precisamos usar callbacks then .... f().then(() => console.log('Finished'));

Agora, de volta ao problema da seção anterior:

// Encapsular a solução em uma função assíncrona async function solution() { // Aguarde a primeira chamada HTTP e imprima o resultado console.log(await rp('http://example.com/')); // Gera as chamadas HTTP sem esperar por elas - execute-as simultaneamente const call2promise = rp('http://example.com/'); // Não espera! const call3promise = rp('http://example.com/'); // Não espera! // Depois que ambos são gerados - espere por ambos const response2 = await call2promise; const resposta3 = espera call3promise; console.log(resposta2); console.log(resposta3); } // Chama a função assíncrona solution().then(() => console.log('Finished'));

Neste código, encapsulamos a solução em uma função async. Isso nos permite usar await por promessas, evitando assim ligue de volta then. Para depois invocar a função async que cria uma promessa para encapsular a lógica para invocar outras promessas.
De fato, no primeiro exemplo (sem async/await), as promessas foram executadas em paralelo e será o mesmo aqui. Observe que não usamos o await que na linha 11-12, bloqueamos a execução aqui até que ambas as promessas sejam concluídas.
Após estas linhas, sabemos que as promessas são encerradas como com o Promise.all(...).then(...) ) do exemplo anterior.
Sob o capô, await/async são traduzidos com promessas e retornos de chamada then. Basicamente, é apenas açúcar sintático para trabalhar com promessas. Toda vez que usamos await, o interpretador cria uma promessa e encapsula o restante das operações do async num ligue de volta then.
Considere o seguinte exemplo:

função assíncrona f() { console.log('Iniciando F'); const resultado = await rp('http://example.com/'); console.log(resultado); }

O fluxo de execução de f está descrito abaixo. Como f é async, ele é executado “paralelamente” à função que o chamou.
Exemplo de diagrama 3
A função f executa e cria uma promessa. Neste ponto, o resto da função é encapsulado em um ligue de volta, então executado no final da promessa.

Manipulação de erros

Na maioria dos exemplos apresentados, assumimos que a promessa foi bem-sucedida e que, portanto, a espera por uma promessa retornou um valor.
Mas esteja ciente de que se uma promessa for rejeitada em uma função async e “esperado” através await, ele lançará uma exceção. É então necessário usar o padrão try/catch para lidar com essas exceções.

async function f() {
    try {
        const promesseResult = await Promise.reject('Error');
    } catch (e){
        console.log(e);
    }
}

Se a função async não trata a exceção, que pode ser causada por uma promessa falhada ou algum outro bug, ele retornará uma promessa rejeitada.

async function f() { // Lança uma exceção const promiseResult = await Promise.reject('Error'); } // Impressora "Erro" f() .then(() => console.log('Sucesso')) .catch(err => console.log(err)); função assíncrona g() { throw "Erro"; } // Impressora "Erro" g() .then(() => console.log('Sucesso')) .catch(err => console.log(err));

Isso nos permite processar promessas rejeitadas por meio do mecanismo padrão de tratamento de erros.

Discussão

Async/await é uma estrutura de linguagem que complementa promessas, nos permite trabalhar com menos clichê. Por outro lado, async/await não substitui as promessas.
Por exemplo, se chamarmos uma função async de uma função normal ou de escopo global, não poderemos usar o await e teremos que usar as promessas.

função assíncrona fAsync() { // return promise.resolve(5) return 5; } // Não podemos usar await fAsync(). Use then/catch fAsync() .then(r => console.log(`resultado é ${r}`));

Normalmente eu tento encapsular a maioria da minha lógica assíncrona em uma ou mais funções async, e eu os chamo do meu código não assíncrono. O que minimiza a quantidade de then/catch retornos de chamada escrever.
O padrão async/await é sintático para ser mais conciso com promessas. Qualquer código com o padrão async/await pode ser reescrito com promessas simples. Em última análise, é uma questão de estilo.
Os acadêmicos gostam de notar que a simultaneidade e o paralelismo são diferentes. Eu deixo você assistir a conversa de Rob Pike sobre o assunto.
Simultaneidade é a capacidade de compor processos independentes (definição geral de processo) para trabalhar em conjunto. Quanto ao paralelismo, é poder executar vários processos simultaneamente.
A simultaneidade se concentra no design e na estrutura do aplicativo, enquanto o paralelismo é um modo de execução.
Por exemplo, vamos pegar um aplicativo multi-thread. A separação da aplicação em vários tópicos define seu modelo de competição. O mapeamento desses processos nos núcleos disponíveis define seu nível de paralelismo.
Um sistema concorrente pode funcionar em um único processador e, neste caso, não é paralelo.

Competição x paralelismo
Fonte: http://nikgrozev.com/2017/10/01/async-await/
Com essas definições em mente, as promessas nos permitem dividir nosso programa em vários modelos simultâneos que podem ou não ser executados em paralelo. Se a execução do JavaScript é paralela ou não depende de sua implementação. Por exemplo, o Nó JS é rosca simples e se uma promessa é limite de CPU, não haverá muito paralelismo. Por outro lado, se compilarmos nosso código em Java Byte code via Nashorn, por exemplo, poderemos, em teoria, mapear as promessas Vinculado à CPU em diferentes core e tem paralelismo. Então, acho que as promessas (manuscritas ou assíncronas/aguardadas) representam um modelo de simultaneidade de aplicativo JavaScript.
Fonte: Await e Async explicados com diagramas e exemplos, Nikolai Grozev
Tradução por João Ribeiro