Um dos maiores desafios ao escrever código de front-end ou código Node.js é lidar com a assincronicidade. Houve uma revolução do gerador original quando pacotes como co nos permitiram escrever código assíncrono de aparência síncrona com construções normais como try
e catch
:
co.wrap (function * () { tentar { yield fetch ('http://some.domain'); } catch (errar) { //lidar com } });
A idade das trevas (assíncrono/espera)
Por volta dessa época, C # e.net começaram a enviar a construção async... await
original que o código assíncrono achatado em uma forma mais familiar:
public static async Task Main () { Tarefadownload=DownloadDocsMainPageAsync (); int bytesLoaded=aguardar download; Console.WriteLine ($"{nameof (Main)}: Download de {bytesLoaded} bytes."); }
Algumas pessoas muito espertas decidiram que o JavaScript deveria adotar as palavras-chave async... await
na linguagem JavaScript. Babel e regenerador transpilaram as construções de palavras-chave em código que usava geradores para atingir o fluxo de trabalho assíncrono. Nodejs deu um passo além e tornou async... await
um cidadão de primeira classe.
O que torna o código async... await
tão atraente é que ele parece síncrono. O código parece parar e esperar até que uma resposta seja retornada ou um erro ocorra. O código pode ser agrupado em um bloco try..catch
familiar.
async... await
ganhou muita força, e a revolução do gerador foi esquecida para o mais limitado async... await
.
Suspender e retomar
O que torna as funções geradoras de JavaScript tão diferentes é que elas não são executadas inicialmente e, em vez disso, retornam um objeto iterador com uma função next
. A execução na função pode ser suspensa e retomada exatamente no ponto em que foi suspensa entre as próximas
chamadas.
Tenho usado o pacote npm thefrontside/effection já há algum tempo.
O Effection utiliza a magia dos geradores para nos permitir escrever código como este:
run (function * () { let socket=new WebSocket ('ws://localhost: 8080'); rendimento throwOnErrorEvent (soquete); rendimento uma vez (soquete,"aberto"); deixe mensagens=produzir uma vez (soquete,"mensagem"); while (true) { deixar mensagem=produzir mensagens.next (); console.log ('Mensagem recebida:', mensagem); } });
Existem algumas abstrações bonitas no código acima que facilitam o caminho para escrever menos código e um código mais simples.
Por exemplo:
rendimento uma vez (soquete,"abrir");
O código acima afirma que a execução não pode prosseguir até que o evento websocket open
tenha ocorrido.
Se estivéssemos fazendo isso em JavaScript normal, seria mais ou menos assim:
const remove=socket.addEventListener ('open', (event)=> { //Continuar });
A essência do gerador
Vamos recapitular rapidamente o que torna os geradores tão poderosos.
Uma função geradora é uma iterator que retorna um objeto que podemos chamar em seguida. Um gerador parece ser uma função, mas se comporta como um iterador .
O que torna os geradores tão poderosos é sua capacidade de suspender e retomar a execução.
A função do gerador everySingleEvenNumber
abaixo ilustra esse recurso:
função
* everySingleEvenNumber () { deixe i=0; while (true) { rendimento i +=2; } } var gen=everySingleEvenNumber (); console.log (gen.next (). valor);//2 console.log (gen.next (). valor);//4 console.log (gen.next (). valor);//6 console.log (gen.next (). valor);//8
A construção while (true)
parece um loop infinito, mas a execução é suspensa após cada yield
e somente retomada quando o iterador próximo
função é chamada no código console.log
.
O valor atual da variável local i
não é redefinido entre cada chamada e é mantido.
Os geradores diferem de async/await, em que a execução desaparece e só retorna quando uma promessa é resolvida ou rejeitada.
Geradores como threads de execução
A capacidade de suspender e retomar funções abre muito mais portas do que async/await fechou em sua rápida adoção.
effection
permite que você crie processos separados como funções geradoras e cuide da desmontagem de todos os processos filho iniciados com effection. Esta técnica é conhecida como simultaneidade estruturada .
Effection expõe um objeto task
que pode gerar
novos processos desanexados
:
main (função * (tarefa: Tarefa) { console.log ('em principal'); task.spawn (function * () { while (true) { produzir sono (100); console.log ('acordado'); } }); produção; })
Cenário do mundo real
Abaixo está uma função flakyConnection
que não se conectará até a quinta tentativa:
deixe tentar=1; function flakyConnection (): Promise <{conectado: booleano}> { retornar nova promessa <{conectado: booleano}> ((resolver)=> { setTimeout (()=> { tentativa ++; resolver ({conectado: tentativa===5}); }, 100); }); }
Para obter uma conexão, o cliente terá que tentar cinco vezes antes de obter sucesso. Um bom código de cliente também incluirá um tempo limite e lançará uma exceção se a operação demorar muito.
Escrever código de sondagem que atinge o tempo limite é um código chato de escrever, mas a efetividade e as qualidades de suspensão e retomada dos geradores tornam essa experiência muito boa:
main (função * (pai: Tarefa) { parent.spawn (function * (child) { child.spawn (function * () { console.log ('preparado para lançar um erro'); produzir sono (8000); jogue um novo erro ('você está sem tempo! Melhor sorte na próxima vez.'); }); while (true) { console.log (`tentativa de conexão $ {tentativa}...`); const {conectado}=rendimento flakyConnection (); if (conectado) { console.log ('estamos conectados!'); return true; } console.log ('sem charuto, tentamos novamente'); produzir sono (2000); } }); produção; });
Um novo processo é anexado ao objeto de tarefa pai
disponibilizado por meio de main
.
O código a seguir elegantemente define um tempo limite que lançará uma exceção se o cliente não puder se conectar após 8000 milissegundos:
child.spawn (function * () { console.log ('preparado para lançar um erro'); produzir sono (8000); jogue um novo erro ('você está sem tempo! Melhor sorte na próxima vez.'); });
A função effection sleep
suspenderá a execução por 8.000 milissegundos. Se o processo pai ainda existir após 8.000 milissegundos, ele lançará uma exceção.
O código a seguir tentará se conectar em intervalos de 200 milissegundos até ter sucesso:
while (true) { console.log (`tentativa de conexão $ {tentativa}...`); const {conectado}=rendimento flakyConnection (); if (conectado) { console.log ('estamos conectados!'); return true; } console.log ('sem charuto, tentamos novamente'); produzir sono (300); }
Este código acima pode continuar em execução até que ocorra uma conexão ou até que a exceção de tempo limite lance, em cujo estágio o efeito irá fechar todos os processos filho.
A execução do código acima resulta nesta saída:
preparado para lançar um erro tentativa de conexão 1... sem charuto, tentamos de novo tentativa de conexão 2... sem charuto, tentamos de novo tentativa de conexão 3... sem charuto, tentamos de novo tentativa de conexão 4... estamos conectados!
Aqui está um repo com o código acima.
Você pode verificar se o tempo limite funciona alterando o código de tempo limite para algo assim:
child.spawn (function * () { console.log ('preparado para lançar um erro'); produzir sono (4000); jogue um novo erro ('você está sem tempo! Melhor sorte na próxima vez.'); });
O tempo limite que ocorre resulta nesta saída:
preparado para lançar um erro tentativa de conexão 1... sem charuto, tentamos de novo tentativa de conexão 2... sem charuto, tentamos de novo Erro: você está sem tempo! Melhor sorte da próxima vez.
É hora da revolução que nunca aconteceu
Ainda uso async/await para tarefas assíncronas simples e únicas sem fluxo de trabalho, mas é um paradigma limitado.
As funções do gerador podem resolver uma série de problemas que nada mais pode. Iniciar e retomar threads de execução é incrivelmente poderoso, e os geradores têm essa funcionalidade integrada e pronta para uso.
Comece! A água está quente.
O post Geradores de JavaScript: The superior async/await apareceu primeiro no LogRocket Blog .