Compreendendo a assincronia em JavaScript [Tradução de Sukhjinder Arora]

Olá Habr! Apresento a você a tradução do artigo “Entendendo o JavaScript Assíncrono” de Sukhjinder Arora.



Do autor da tradução: Espero que a tradução deste artigo ajude você a se familiarizar com algo novo e útil. Se o artigo o ajudou, não seja preguiçoso e agradeça ao autor do original. Não pretendo ser um tradutor profissional, estou apenas começando a traduzir artigos e ficarei feliz em receber algum feedback significativo.

JavaScript é uma linguagem de programação de thread único na qual apenas uma coisa pode ser executada por vez. Ou seja, em um único encadeamento, o mecanismo JavaScript pode processar apenas uma instrução por vez.

Embora os idiomas de thread único facilitem a escrita do código, já que você não precisa se preocupar com problemas de simultaneidade, isso também significa que você não poderá executar operações longas, como acessar a rede sem bloquear o segmento principal.

Envie uma solicitação de API para alguns dados. Dependendo da situação, o servidor pode levar algum tempo para processar sua solicitação, enquanto a execução do fluxo principal será bloqueada, devido ao qual sua página da web deixará de responder às solicitações.

É aqui que a assincronia do JavaScript entra em cena. Usando a assincronia JavaScript (retornos de chamada, promessas e assíncrono / espera), você pode executar solicitações de rede longas sem bloquear o segmento principal.

Embora não seja necessário aprender todos esses conceitos para ser um bom desenvolvedor de JavaScript, é útil conhecê-los.

Então, sem mais delongas, vamos começar.

Como funciona o javascript síncrono?


Antes de começarmos o trabalho do JavaScript assíncrono, vamos primeiro entender como o código síncrono é executado dentro do mecanismo JavaScript. Por exemplo:

const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 

Para entender como o código acima é executado dentro do mecanismo JavaScript, precisamos entender o conceito do contexto de execução e a pilha de chamadas (também conhecida como pilha de execução).

Contexto de execução


O contexto de execução é um conceito abstrato do ambiente no qual o código é avaliado e executado. Sempre que qualquer código é executado em JavaScript, ele é executado no contexto de execução.

O código da função é executado dentro do contexto de execução da função e o código global, por sua vez, é executado dentro do contexto de execução global. Cada função tem seu próprio contexto de execução.

Pilha de chamadas


Uma pilha de chamadas é uma pilha com uma estrutura LIFO (Last in, First Out, first used), usada para armazenar todos os contextos de execução criados durante a execução do código.

O JavaScript possui apenas uma pilha de chamadas, pois é uma linguagem de programação de thread único. A estrutura LIFO significa que os elementos só podem ser adicionados e removidos da parte superior da pilha.

Vamos agora voltar ao snippet de código acima e tentar entender como o mecanismo JavaScript o executa.

 const second = () => { console.log('Hello there!'); } const first = () => { console.log('Hi there!'); second(); console.log('The End'); } first(); 



Então o que aconteceu aqui?


Quando o código começou a ser executado, um contexto de execução global foi criado (representado como main () ) e adicionado à parte superior da pilha de chamadas. Quando a chamada para a primeira função () é encontrada, ela também é adicionada ao topo da pilha.

Em seguida, console.log ('Olá!') É colocado na parte superior da pilha de chamadas, após a execução é removido da pilha. Depois disso, chamamos a segunda função () , para que ela seja colocada no topo da pilha.

O console.log ('Olá!') é adicionado ao topo da pilha e é removido após a conclusão da execução. A segunda função () é concluída, também é removida da pilha.

console.log ('The End') foi adicionado ao topo da pilha e removido no final. Depois disso, a primeira função () termina e também é removida da pilha.

A execução do programa termina, portanto, o contexto de chamada global ( main () ) é removido da pilha.

Como o JavaScript assíncrono funciona?


Agora que temos um entendimento básico da pilha de chamadas e como funciona o JavaScript síncrono, voltemos ao JavaScript assíncrono.

O que está bloqueando?


Vamos supor que estamos processando o processamento de imagem ou a solicitação de rede de forma síncrona. Por exemplo:

 const processImage = (image) => { /** *    **/ console.log('Image processed'); } const networkRequest = (url) => { /** *      **/ return someData; } const greeting = () => { console.log('Hello World'); } processImage(logo.jpg); networkRequest('www.somerandomurl.com'); greeting(); 

O processamento da imagem e a solicitação de rede levam tempo. Quando a função processImage () é chamada, sua execução leva algum tempo, dependendo do tamanho da imagem.

Quando a função processImage () é concluída, ela é removida da pilha. Depois disso, a função networkRequest () é chamada e adicionada à pilha. Isso novamente levará algum tempo antes de concluir a execução.

No final, quando a função networkRequest () é executada, a função greeting () é chamada, pois contém apenas o método console.log , e esse método geralmente é rápido, e a função greeting () será executada e finalizada instantaneamente.

Como você pode ver, precisamos aguardar a conclusão da função (como processImage () ou networkRequest () ). Isso significa que essas funções bloqueiam a pilha de chamadas ou o encadeamento principal. Como resultado, não podemos executar outras operações até que o código acima seja executado.

Então, qual é a solução?


A solução mais simples são as funções de retorno de chamada assíncronas. Nós os usamos para tornar nosso código sem bloqueio. Por exemplo:

 const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); 

Aqui eu usei o método setTimeout para simular uma solicitação de rede. Lembre-se de que setTimeout não faz parte do mecanismo JavaScript, faz parte da chamada API da Web (no navegador) e APIs do C / C ++ (no node.js).

Para entender como esse código é executado, precisamos lidar com mais alguns conceitos, como o loop de eventos e a fila de retorno de chamada (também conhecida como fila de tarefas ou fila de mensagens).



O loop de eventos, a API da Web e a fila de mensagens / fila de tarefas não fazem parte do mecanismo JavaScript; eles fazem parte do tempo de execução JavaScript JavaScript ou do tempo de execução JavaScript no Nodejs (no caso do Nodejs). No Nodejs, as APIs da web são substituídas pelas APIs de C / C ++.

Agora, vamos voltar ao código acima e ver o que acontece no caso de execução assíncrona.

 const networkRequest = () => { setTimeout(() => { console.log('Async Code'); }, 2000); }; console.log('Hello World'); networkRequest(); console.log('The End'); 



Quando o código acima é carregado no navegador, o console.log ('Hello World') é adicionado à pilha e removido da mesma após a conclusão da execução. Em seguida, é encontrada uma chamada para a função networkRequest () , que é adicionada ao topo da pilha.

Em seguida, a função setTimeout () é chamada e colocada na parte superior da pilha. A função setTimeout () possui 2 argumentos: 1) uma função de retorno de chamada e 2) tempo em milissegundos.

setTimeout () inicia um timer por 2 segundos em um ambiente de API da web. Nesse ponto, setTimeout () é concluído e é removido da pilha. Depois disso, console.log ('The End') é adicionado à pilha, executado e removido dela após a conclusão.

Enquanto isso, o cronômetro expirou, agora o retorno de chamada é adicionado à fila de mensagens. Mas o retorno de chamada não pode ser executado imediatamente e é aqui que o ciclo de processamento de eventos entra no processo.

Loop de eventos


A tarefa do loop de eventos é acompanhar a pilha de chamadas e determinar se está vazia ou não. Se a pilha de chamadas estiver vazia, o loop de eventos procurará na fila de mensagens para ver se há retornos de chamada que estão aguardando para serem concluídos.

No nosso caso, a fila de mensagens contém um retorno de chamada e a pilha de execução está vazia. Portanto, o loop de eventos adiciona um retorno de chamada à parte superior da pilha.

Após o console.log ('Código assíncrono') ser adicionado à parte superior da pilha, executado e removido dela. Nesse ponto, o retorno de chamada é concluído e removido da pilha e o programa está completamente concluído.

Eventos DOM


A fila de mensagens também contém retornos de chamada de eventos DOM, como cliques e eventos de teclado. Por exemplo:

 document.querySelector('.btn').addEventListener('click',(event) => { console.log('Button Clicked'); }); 

No caso de eventos DOM, o manipulador de eventos é cercado pela API da web, aguardando um evento específico (neste caso, um clique) e, quando esse evento ocorre, a função de retorno de chamada é colocada na fila de mensagens, aguardando sua execução.

Aprendemos como são executados retornos de chamada assíncronos e eventos DOM, que usam uma fila de mensagens para armazenar retornos de chamada que aguardam execução.

Fila do ES6 MicroTask


Nota autor da tradução: no artigo, o autor usou a fila de mensagens / tarefas e a fila de tarefas / micro-tarefas, mas se você traduzir a fila de tarefas e a fila de tarefas, em teoria, acontece a mesma coisa. Conversei com o autor da tradução e decidi simplesmente omitir o conceito de fila de empregos. Se você tem alguma opinião sobre isso, estou esperando por você nos comentários

Link para a tradução do artigo por promessas do mesmo autor


O ES6 introduziu o conceito de fila de microtask, usada pelo Promises em JavaScript. A diferença entre a fila de mensagens e a fila de microtask é que a fila de microtask tem uma prioridade mais alta que a fila de mensagens, o que significa que “promessas” dentro da fila de microtask serão executadas antes dos retornos de chamada na fila de mensagens.

Por exemplo:

 console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End'); 

Conclusão:

 Script start Script End Promise resolved setTimeout 

Como você pode ver, a “promessa” foi executada antes do setTimeout , tudo isso porque a resposta da “promessa” é armazenada dentro da fila de microstase, que tem uma prioridade mais alta que a fila de mensagens.

Vejamos o exemplo a seguir, desta vez 2 "promessas" e 2 setTimeout :

 console.log('Script start'); setTimeout(() => { console.log('setTimeout 1'); }, 0); setTimeout(() => { console.log('setTimeout 2'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => console.log(res)) .catch(err => console.log(err)); console.log('Script End'); 

Conclusão:

 Script start Script End Promise 1 resolved Promise 2 resolved setTimeout 1 setTimeout 2 

E, novamente, nossas duas "promessas" foram executadas antes dos retornos de chamada no setTimeout , pois o loop de processamento de eventos considera as tarefas da fila de microtask mais importantes do que as tarefas da fila de mensagens / fila de tarefas.

Se outro "Promise" aparecer na fila de microtask durante a execução das tarefas, ele será adicionado ao final dessa fila e executado antes dos retornos de chamada da fila de mensagens, e não importa quanto tempo eles esperem pela execução.

Por exemplo:

 console.log('Script start'); setTimeout(() => { console.log('setTimeout'); }, 0); new Promise((resolve, reject) => { resolve('Promise 1 resolved'); }).then(res => console.log(res)); new Promise((resolve, reject) => { resolve('Promise 2 resolved'); }).then(res => { console.log(res); return new Promise((resolve, reject) => { resolve('Promise 3 resolved'); }) }).then(res => console.log(res)); console.log('Script End'); 

Conclusão:

 Script start Script End Promise 1 resolved Promise 2 resolved Promise 3 resolved setTimeout 

Portanto, todas as tarefas da fila de microtask serão concluídas antes das tarefas da fila de mensagens. Ou seja, o loop de processamento de eventos limpará primeiro a fila de microtask e só então começará a executar retornos de chamada da fila de mensagens.

Conclusão


Assim, aprendemos como o JavaScript assíncrono funciona e conceitos: pilha de chamadas, loop de eventos, fila de mensagens / fila de tarefas e fila de microtask que compõem o tempo de execução do JavaScript

Source: https://habr.com/ru/post/pt439620/


All Articles