O JavaScript é uma linguagem single thread, ou seja, possui apenas uma thread de execução e por isso depende de um mecanismo chamado Event Loop para coordenar a execução de operações assíncronas. Algumas realizadas em paralelo por recursos externos como Web APIs, outras simplesmente adiadas para depois que o código síncrono atual terminar.
Para entender como o Event loop funciona vamos entender rapidamente as seguintes estruturas utilizadas pelo JS:
- Call Stack
- Web APIs
- Task queue
- Microtask queue
- Event loop
Call Stack
É a estrutura de dados que segue o princípio LIFO (Last in, First out) utilizada na memória para organizar e processar a execução do código. Sempre que invocamos uma função utilizando (), como por exemplo firstFn(), ela é empilhada na call stack. Caso a firstFn invoque uma outra função secondFn(), essa última será empilhada em cima da firstFn e será executada e removida primeiro, seguindo o princípio LIFO.
Importante frisar que qualquer código que não foi declarado dentro de uma função também será empilhado na call stack, pois no momento que um arquivo JS é lido, o motor JS cria o Global Execution Context (Contexto de Execução Global), sendo a primeira coisa a ser empilhada na Call stack.
Web APIs
O JavaScript é uma linguagem de programação e sabe apenas processar dados e lógica. Não sabe o que é um clique no mouse, rede, timer…, quem é reponsável por lidar com isso é o browser, que tem acesso a rede, tela, microfone e demais recursos.
As Web APIs são a ponte, ou seja, um conjunto de funcionalidades que o browser disponibiliza para o código JS utilizar. Com isso, qualquer processo que é executado por meio das Web APIs é um trabalho externo ao JS, sendo realizado em uma thread separada, fora da thread do JavaScript.
Web APIs é utilizada no contexto onde o JavaScript está rodando no navegador. Quando o JS é utilizado no Node.js o equivalente a Web APIs é a libuv, uma biblioteca desenvolvida em C para possibilitar input e output assíncronos.
Diferente da Web APIs, o Node.js possui a nextTick Queue, uma fila exclusiva com prioridade ainda maior que a Microtask Queue, gerenciada pelo próprio runtime do Node.js.
Task Queue
Estrutura de dados Fila que segue o princípio FIFO (First in, First out) utilizado como memória para receber todos os callbacks que vieram dos processamentos realizados externamente pela Web APIs. Com isso, se utilizarmos recursos do browser através das Web APIs, quando o processamento for finalizado, o callback virá para a Task Queue para ser enviada pelo Event Loop a Call Stack assim que a mesma estiver vazia.
Task Queue também pode ser encontrada como Macrotask Queue ou Callback Queue, todas se referem a mesma estrutura.
Confira algumas das Web APIs:
- fetch()
- setTimeout()
- setInterval()
- addEventListener()
Microtask Queue
Estrutura exatamente igual a Callback Queue, diferindo em apenas dois pontos em como é utilizada e consumida pelo Event Loop:
- A Microtask existe para receber callbacks cujo resultado já está disponível dentro do próprio motor JS, sem necessidade de processamento externo. Não há trabalho sendo feito, apenas a execução do callback é inserida na fila, sendo adiado pra depois que o código síncrono atual terminar;
- Essa queue tem uma prioridade maior comparada a Callback queue. O Event loop sempre esvazia totalmente a Microtask Queue antes de pegar um item da Callback Queue. Após mover um item da Callback Queue para a Call Stack o Event Loop volta a verificar a Microtask Queue primeiro, mantendo a prioridade.
A Microtask Queue também pode ser encontrada como Job Queue
Confira alguma das formas de fazer um processamento assíncrono, enviando para a Microtask queue para ser resolvido depois:
- Promise.then()
- Promise.catch()
- Promise.finally()
- async/await
- queueMicrotask()
Event Loop
O Event Loop é um loop infinito que coordena a Call Stack, Microtask Queue e Callback Queue. Sempre que a Call Stack está vazia ele move o próximo callback para ela na ordem correta: esvazia toda a Microtask Queue primeiro para então poder pegar um callback da Callback Queue e recomeçar todo esse fluxo novamente.
O Fluxo completo
Para entendermos como todo o fluxo funciona para possibilitar que uma linguagem single thread consiga fazer um processamento em paralelo com a Web APIs ou adiar uma execução, vamos conferir o seguinte código:
function soma(a, b) {
return a + b;
}
console.log('Síncrono 1');
console.log(soma(5, 5));
setTimeout(() => {
console.log("Assíncrono 2")
}, 0);
Promise.resolve("Assíncrono 1").then((item) => {
console.log(item)
});
console.log('Síncrono 2')
Confira a ordem como a Call Stack será empilhada:
- No momento que esse código for lido, será criado o Global Execution Context, contendo o todo, sendo empilhado na base da Call Stack;
- O
console.log(’Síncrono 1’)será empilhado, executado e removido da pilha; - O
console.log()que possui a soma(5, 5) será empilhado. Osoma(5, 5)será empilhado em seguida, executado e removido. Com isso, oconsole.log(10)será executado e removido da pilha; - O
setTimeout(callback, 0)é empilhado, registra o callback na Web API e é removido;- O processamento ocorre na thread da Web API paralelamente enquanto o fluxo continua em andamento. Quando finalizado, o callback é enviado para a Task Queue que só será verificado pelo Event Loop após a Microtask Queue estar completamente vazia
- O
Promise.resolve(”Assíncrono 1”)é empilhado, registra o callback na Microtask Queue e é removido; - O
console.log(’Síncrono 2’)é empilhado, executado e removido da pilha; - Agora que todo o documento foi lido, o Global Execution Context é finalmente removido da pilha;
- Com a Call Stack vazia o Event Loop passa a esvaziar a Microtask Queue, movendo o primeiro callback para a Call Stack, executando o
console.log('Assíncrono 1')e removendo-o da pilha - Com a Call Stack e a Microtask Queue vazia o Event Loop move o primeiro item da Task Queue:
console.log(’Assíncrono 2’), sendo executado e removido da pilha.
Perceba que, mesmo que o setTimeout ocorra antes do Promise.resolve, como ele é inserido na Task Queue após o timer expirar na Web API, ele acaba sendo executado somente após a Promise.resolve, pelo fato do Event Loop dar prioridade a Microtask Queue.
Nosso fluxo termina aqui, mas caso a Microtask Queue tivesse um novo callback registrado, o Event Loop iria priorizar movê-lo para a Call Stack antes de checar a Task Queue novamente.
Referências
- JavaScript Execution Model
- Using microtasks in JavaScript with queueMicrotask
- In depth: Microtasks and the JavaScript Runtime Environment
Créditos de imagem
Foto de Aleksandr Popov na Unsplash




Top comments (0)