O modelo de execução do JavaScript é sutil e fácil de ser mal interpretado. Aprender sobre o loop de eventos em seu núcleo pode ajudar.
JavaScript é uma linguagem de encadeamento único, criada para lidar com tarefas uma de cada vez. No entanto, o loop de eventos permite que o JavaScript manipule eventos e retornos de chamada de forma assíncrona, emulando sistemas de programação simultâneos. Isso garante o desempenho de seus aplicativos JavaScript.
O que é o loop de eventos do JavaScript?
O loop de eventos do JavaScript é um mecanismo executado em segundo plano em todos os aplicativos JavaScript. Ele permite que o JavaScript manipule tarefas em sequência sem bloquear seu thread de execução principal. Isto é referido como programação assíncrona.
O loop de eventos mantém uma fila de tarefas a serem executadas e alimenta essas tarefas à direita API web para execução um de cada vez. O JavaScript rastreia essas tarefas e lida com cada uma de acordo com o nível de complexidade da tarefa.
Compreender a necessidade do loop de eventos JavaScript e da programação assíncrona. Você precisa entender qual problema ele essencialmente resolve.
Pegue este código, por exemplo:
functionlongRunningFunction() {
// This function does something that takes a long time to execute.
for (var i = 0; i < 100000; i++) {
console.log("Hello")
}
}functionshortRunningFunction(a) {
return a * 2 ;
}functionmain() {
var startTime = Date.now();
longRunningFunction();
var endTime = Date.now();// Prints the amount of time it took to execute functions
console.log(shortRunningFunction(2));
console.log("Time taken: " + (endTime - startTime) + " milliseconds");
}
main();
Este código primeiro define uma função chamada longRunningFunction(). Esta função fará algum tipo de tarefa complexa e demorada. Neste caso, ele realiza uma para loop iterando mais de 100.000 vezes. Isso significa que console.log("Olá") corre 100.000 vezes.
Dependendo da velocidade do computador, isso pode demorar e bloquear shortRunningFunction() desde a execução imediata até a conclusão da função anterior.
Para contextualizar, aqui está uma comparação do tempo necessário para executar as duas funções:
E então o solteiro shortRunningFunction():
A diferença entre uma operação de 2.351 milissegundos e uma operação de 0 milissegundos é óbvia quando você pretende criar um aplicativo de alto desempenho.
Como o loop de eventos ajuda no desempenho do aplicativo
O loop de eventos possui diferentes estágios e partes que contribuem para o funcionamento do sistema.
A Pilha de Chamadas
A pilha de chamadas do JavaScript é essencial para saber como o JavaScript lida com as chamadas de funções e eventos do seu aplicativo. O código JavaScript compila de cima para baixo. No entanto, Node.js, ao ler o código, Node.js atribuirá chamadas de função de baixo para cima. À medida que lê, ele empurra as funções definidas como quadros para a pilha de chamadas, um por um.
A pilha de chamadas é responsável por manter o contexto de execução e a ordem correta das funções. Ele faz isso operando como uma pilha LIFO (Last-In-First-Out).
Isso significa que o último quadro de função que seu programa coloca na pilha de chamadas será o primeiro a sair da pilha e ser executado. Isso garantirá que o JavaScript mantenha a ordem correta de execução da função.
O JavaScript removerá cada quadro da pilha até que esteja vazio, o que significa que todas as funções terminaram de ser executadas.
Libuv Web APIName
No centro dos programas assíncronos do JavaScript está libuv. A biblioteca libuv é escrita na linguagem de programação C, que pode interagir com o sistema operacional APIs de baixo nível. A biblioteca fornecerá várias APIs que permitem que o código JavaScript seja executado em paralelo com outros código. APIs para criar threads, uma API para comunicação entre threads e uma API para gerenciar a sincronização de threads.
Por exemplo, quando você usa setTimeout em Node.js para pausar a execução. O temporizador é configurado por meio de libuv, que gerencia o loop de eventos para executar a função de retorno de chamada assim que o atraso especificado tiver passado.
Da mesma forma, quando você executa operações de rede de forma assíncrona, libuv lida com essas operações de forma não bloqueante maneira, garantindo que outras tarefas possam continuar processando sem esperar pela operação de entrada/saída (E/S) para fim.
O retorno de chamada e a fila de eventos
A fila de retorno de chamada e evento é onde as funções de retorno de chamada aguardam a execução. Quando uma operação assíncrona é concluída a partir do libuv, sua função de retorno de chamada correspondente é adicionada a essa fila.
Veja como fica a sequência:
- O JavaScript move tarefas assíncronas para libuv para que ele as manipule e continue a lidar com a próxima tarefa imediatamente.
- Quando a tarefa assíncrona termina, o JavaScript adiciona sua função de retorno de chamada à fila de retorno de chamada.
- O JavaScript continua executando outras tarefas na pilha de chamadas até terminar tudo na ordem atual.
- Quando a pilha de chamadas estiver vazia, o JavaScript examinará a fila de retorno de chamada.
- Se houver um retorno de chamada na fila, ele coloca o primeiro na pilha de chamadas e o executa.
Dessa forma, as tarefas assíncronas não bloqueiam o thread principal e a fila de retorno de chamada garante que seus retornos de chamada correspondentes sejam executados na ordem em que foram concluídos.
O Ciclo de Loop de Eventos
O loop de eventos também tem algo chamado fila de microtarefas. Essa fila especial no loop de eventos contém microtarefas agendadas para serem executadas assim que a tarefa atual na pilha de chamadas for concluída. Essa execução ocorre antes da próxima renderização ou iteração do loop de eventos. Microtarefas são tarefas de alta prioridade com precedência sobre tarefas regulares no loop de eventos.
Uma microtarefa geralmente é criada ao trabalhar com Promises. Sempre que uma Promise resolve ou rejeita, seu correspondente .então() ou .pegar() callbacks ingressa na fila de microtarefas. Você pode usar essa fila para gerenciar tarefas que precisam de execução imediata após a operação atual, como atualizar a interface do usuário de seu aplicativo ou lidar com alterações de estado.
Por exemplo, um aplicativo da Web que executa a busca de dados e atualiza a IU com base nos dados recuperados. Os usuários podem acionar essa busca de dados clicando em um botão repetidamente. Cada clique no botão inicia uma operação assíncrona de recuperação de dados.
Sem as microtarefas, o loop de eventos para esta tarefa funcionaria da seguinte forma:
- O usuário clica no botão repetidamente.
- Cada clique no botão aciona uma operação assíncrona de busca de dados.
- À medida que as operações de busca de dados são concluídas, o JavaScript adiciona seus retornos de chamada correspondentes à fila de tarefas regular.
- O loop de eventos começa a processar tarefas na fila de tarefas regular.
- A atualização da interface do usuário com base nos resultados da busca de dados é executada assim que as tarefas regulares permitem.
No entanto, com microtarefas, o loop de eventos funciona de maneira diferente:
- O usuário clica no botão repetidamente e aciona uma operação assíncrona de busca de dados.
- À medida que as operações de busca de dados são concluídas, o loop de eventos adiciona seus retornos de chamada correspondentes à fila de microtarefas.
- O loop de eventos inicia o processamento de tarefas na fila de microtarefas imediatamente após a conclusão da tarefa atual (clique no botão).
- A atualização da interface do usuário com base nos resultados da busca de dados é executada antes da próxima tarefa regular, proporcionando uma experiência de usuário mais responsiva.
Aqui está um exemplo de código:
const fetchData = () => {
returnnewPromise(resolve => {
setTimeout(() => resolve('Data from fetch'), 2000);
});
};
document.getElementById('fetch-button').addEventListener('click', () => {
fetchData().then(data => {
// This UI update will run before the next rendering cycle
updateUI(data);
});
});
Neste exemplo, cada clique no botão "Buscar" chama buscarData(). Cada operação de busca de dados é agendada como uma microtarefa. Com base nos dados buscados, a atualização da interface do usuário é executada imediatamente após a conclusão de cada operação de busca, antes de qualquer outra tarefa de renderização ou loop de eventos.
Isso garante que os usuários vejam os dados atualizados sem nenhum atraso devido a outras tarefas no loop de eventos.
O uso de microtarefas em cenários como esse pode impedir a instabilidade da interface do usuário e fornecer interações mais rápidas e suaves em seu aplicativo.
Implicações do loop de eventos para o desenvolvimento da Web
Entender o loop de eventos e como usar seus recursos é essencial para criar aplicativos de alto desempenho e responsivos. O loop de eventos fornece recursos assíncronos e paralelos, para que você possa lidar com tarefas complexas com eficiência em seu aplicativo sem comprometer a experiência do usuário.
O Node.js fornece tudo o que você precisa, incluindo web workers para obter mais paralelismo fora do thread principal do JavaScript.