O que é o Event Loop? Call Stack, Task Queue e Microtasks no JavaScript
Call Stack
A Call Stack é um estrutura que guarda a ordem de execução das funções. Isso garante que o JavaScript saiba onde começar ou continuar após a execução das funções.
Sempre que uma função é executada, um frame é criado e adicionado ao topo da call-stack.
- Quando o programa começa, a call stack está vazia.
- Quando uma função retorna, ela é removida da call stack.
Um frame contém todas as informações daquela função, variáveis locais e argumentos.
Execução da call stack
function c() {
console.log('c')
}
function b() {
console.log('bc')
c()
console.log('cb')
}
function a() { b() }
a();
A call stack será preenchida e esvaziada conforme a execução do programa:
> a é colocado na pilha
> b é colocado na pilha
> b logs 'bc'
> c é colocado na pilha
> c logs 'c'
> c é removido da pilha
> b logs 'cb'
> b é removido da pilha
> a é removido da pilha
Então durante a execução da função c(), o JavaScript sabe todas as funções anteriores que levaram a c(). Quando a função c() termina, ela é removida da call stack, e o processo repete para b().
Esse comportamento é o que define a call stack como uma pilha.
Task Queue
A task-queue (event-loop, queue, macrotask-queue) é uma estrutura que guarda os callbacks que vão ser executados no futuro (enquanto a Call Stack armazena as funções que estão sendo executadas no momento).
O objetivo da task-queue é possibilitar a execução de tarefas demoradas em background sem bloquear a execução de outras tarefas. Isso é necessário porque o JavaScript é single-threaded, executando somente uma tarefa por vez.
function f() { console.log('f') }
function e() { setTimeout(f, 0) }
function d() { console.log('d') }
function c() { console.log('c') }
function b() { console.log('b') }
function a() { console.log('a') }
a();
setTimeout(e, 100);
setTimeout(c, 100);
setTimeout(b, 0);
d();
A task-queue e a Call Stack trabalham junto conforme a execução do programa:
a => alocado na call-stack
a => logs 'a'
a => removido da call-stack
e => agenda uma alocação na task-queue em 100ms
c => agenda uma alocação na task-queue em 100ms
b => alocado na task-queue (0ms)
d => alocado na call-stack
d => logs 'd'
d => removido da call-stack
// A execução chegou ao fim, a call-stack está vazia.
// No browser, esse é o momento de rerender (repaint, reflow)
b => transferida da task-queue para a call-stack
b => logs 'b'
b => removido da call-stack
// call-stack vazia, rerender.
// espera 100ms
e => alocado na task-queue
e => transferida da task-queue para a call-stack
e => removido da call-stack
// call-stack vazia, rerender
f => alocado na task-queue (0 ms) // pela execução de e()
f => transferida da task-queue para a call-stack
f => logs 'f'
f => removido da call-stack
// call-stack vazia, rerender
c => alocado na task-queue
c => transferida da task-queue para a call-stack
c => logs 'c'
c => removido da call-stack
Note que a função f() foi alocada antes que a função c(), mesmo o timeout sendo 100ms para ambas c() e e(). Isso acontece pois o setTimeout(fn, 0) força uma alocação na task-queue imediata, mesmo que o tempo de outros timeouts já tenha passado.
Microtask Queue
A microtask-queue (job-queue, queue, promise-queue) é uma estrutura semelhante a Task Queue, adicionada na ECMAScript 2015, que guarda tasks criadas a partir de Promises (Promise.then, catch, finally) e queueMicrotask. Essas tasks serão executadas no futuro, com prioridade.[^1]
As microtasks tem prioridade de execução em relação as tasks: O agente prioriza mover microtasks para Call Stack, e após isso, enquanto a microtask-queue não estiver vazia, nenhuma outra ação é executada (nem mesmo re-renderizar).
function ab() {
console.log('ab');
return Promise.resolve()
}
function a() {
setTimeout(e, 0)
console.log('a')
}
function b() {
console.log('b')
}
function c() {
console.log('c')
}
function e() {
console.log('e')
}
ab().then(a).then(b)
setTimeout(c, 0)
Agora, a Microtask Queue (Ou promise Queue), a Task Queue (Ou só Queue) e a Call Stack trabalham juntas na execução:
ab => alocado na call-stack
ab => logs 'ab'
a => alocado na microtask-queue
ab => removido da call-stack
c => alocado na task-queue
// Aqui a call-stack ficou vazia, contudo, como uma
// microtask tem prioridade em relação ao rerender
// ela foi transferida primeiro.
a => transferida da microtask-queue para a call-stack
e => alocado na task-queue
a => logs 'a'
a => removido da call-stack
b => alocado na microtask-queue
// novamente call-stack vazia, mas microtasks tem prioridade
b => transferido da microtask-queue para a call-stack
b => logs 'b'
b => removido da call-stack
// A microtask-queue está vazia e a call-stack também.
// No browser, esse é o momento de rerender (repaint, reflow)
c => transferida da task-queue para a call-stack
c => logs 'c'
c => removido da call-stack
// call-stack vazia, rerender
e => transferida da task-queue para a call-stack
e => logs 'e'
e => removido da call-stack
// call-stack vazia, rerender
Não existe limite de queues que o browser/node.js podem ter. No navegador, existe pelo menos uma DOM queue e uma Timer Queue, por exemplo. No entanto, só pode existir somente uma microtask-queue (de acordo com a spec), todas as outras queues serão do tipo task-queue.
Event Loop
O termo event-loop se refere ao mecanismo completo de funcionamento das Tasks Queue, da Microtask Queue, da Call Stack e do algoritmo de looping que processa as tasks.
while (Scheduler.waitForTask()) {
const taskQueue = Scheduler.selectTaskQueue();
if (taskQueue.hasNextTask()) {
taskQueue.processNextTask();
}
const microtaskQueue = Scheduler.microTaskQueue;
while (microtaskQueue.hasNextMicrotask()) {
microtaskQueue.processNextMicrotask();
}
rerender(); // only in browser
}
- Uma iteração de task é chamada de tick.
- As tasks e microtasks são processadas uma de cada vez.
- A task e a microtask em execução tem acesso exclusivo a Call Stack.
- Uma task/microtask é considerada completa quando a sua Call Stack está vazia.
- Uma task/microtask pode colocar alocar outras tasks/microtasks nas queues.
O que significa "acesso exclusivo"?
Cada task é executada por completo antes que outra task comece a ser executada. Então duas tasks nunca serão executadas ao mesmo tempo.
let i;
setTimeout(() => {
i++;
console.log(i)
}, 0)
setTimeout(() => {
i++;
console.log(i)
}, 0)
O que acontece é:
i++ => console.log(i) => i++ => console.log(i)
E nunca:
i++ => i++ => console.log(i) => console.log(i)
Congelar a Event-Loop
Por causa da execução exclusiva, se uma task demorar indefinidamente, outras tasks nunca serão executadas e o programa vai travar.
Contudo, o browser fica de olho no tempo de execução das tasks e pergunta ao usuário se ele quer abortar tasks demoradas demais.
Obrigado por ler!!!
Espero que esse artigo tenha ajudado você a entender melhor o que é o Event Loop. Se houver algum comentário, notou algum erro, ou quer falar comigo, entre em contato pelas minhas redes sociais ou e-mail no card ao lado ou embaixo.