O JavaScript possui um modelo de concorrência baseado em um event loop (laço de eventos, em português), responsável pela execução do código, coleta e processamento de eventos e execução de subtarefas enfileiradas. Este modelo é bem diferente de outras linguagens, como C ou Java, por exemplo.
Conceitos de runtime (tempo de execução)
Os próximos tópicos irão explicar teoricamente o modelo. Interpretadores modernos de JavaScript implementam e otimizam fortemente as semânticas descritas.
Representação visual
Pilha (Stack)
As chamadas de funções criam uma pilha de frames (quadros).
function foo(b) {
var a = 10;
return a + b + 11;
}
function bar(x) {
var y = 3;
return foo(x * y);
}
console.log(bar(7)); //retorna 42
Quando chamamos a função bar
, o primeiro frame é criado contendo argumentos e variáveis locais de bar
. Quando a função bar
chama foo
, o segundo frame é criado e é colocado no topo da pilha contendo os argumentos e a variáveis locais de foo
. Quando foo
retorna, o frame do topo é removido da pilha (deixando apenas o frame da chamada de bar
). Quando bar
retorna, a pilha fica vazia.
Heap
Os objetos são alocados em um heap (acervo), que é apenas um nome para denotar uma grande região não estruturada da memória.
Fila (Queue)
O runtime do JavaScript contém uma fila de mensagens, que é uma lista de mensagens a serem processadas. Para cada mensagem, é associada uma função que é chamada para manipular a mensagem.
Em algum ponto durante o event loop, o runtime começa a manipular as mensagens na fila, iniciando com a mais antiga. Para fazer isso, a mensagem é removida da fila e sua função correspondente é chamada junto com a mensagem. Como de costume, chamar uma função cria uma nova pilha de frame para o uso dessa função.
O processamento de funções continua até que a pilha fique novamente vazia, então o event loop processará a próxima mensagem na fila (se houver uma).
Event loop
Event loop tem esse nome por causa da forma que normalmente é implementado, geralmente é semelhante a:
while (queue.waitForMessage()) {
queue.processNextMessage();
}
queue.waitForMessage
aguarda, de maneira síncrona, receber uma mensagem, se não houver nenhuma atualmente.
"Run-to-completion"
Cada mensagem é processada completamente antes de outra mensagem ser processada. Isso oferece um bom fundamento ao pensar sobre o seu software, incluindo o fato de que, independente de quando uma função é executada, ela não pode ser interrompida, ela será executada por completo, antes que outro código execute (e modifique dados que a função manipule). Isso é diferente do C, por exemplo, no qual uma função que está sendo executada em uma thread, pode ser interrompida a qualquer momento para executar um outro código em outra thread.
O lado negativo deste modelo é que se uma mensagem levar muito tempo para ser finalizada, a aplicação web fica indisponível para processar as interações do usuário, como cliques ou rolagens. O navegador mitiga este problema através do aviso: "Um script desta página pode estar ocupado". Uma boa prática a seguir é fazer o processamento de mensagens curtas, e se possível, dividir uma mensagem em múltiplas mensagens.
Adicionando mensagens
Nos navegadores, as mensagens são adicionadas a qualquer momento que um evento é acionado, se este possuir um "listener". Caso não possua, o evento será ignorado. Assim, um clique em um elemento com um manipulador de eventos de clique adicionará uma mensagem, igualmente como qualquer outro evento.
A função setTimeout
é chamada com 2 argumentos: uma mensagem para adicionar à fila (queue) e um valor em tempo (opcional, padrão é 0). O valor em tempo representa o intervalo (mínimo) com que a mensagem será realmente enviada a fila. Se não houver outra mensagem na fila, a mensagem será processada logo após o intervalo, no entanto, se houver mensagens, a mensagem setTimeout
terá que esperar até que outras mensagens sejam finalizadas. Por esse motivo, o segundo argumento indica um tempo mínimo e não um tempo garantido.
Aqui está um exemplo que demonstra esse conceito (setTimeout
não é executado imediatamente após o temporizador expirar):
const s = new Date().getSeconds();
setTimeout(function() {
// imprime "2", o que significa que o callback não é chamado imediatamente após 500 milissegundos.
console.log("Ran after " + (new Date().getSeconds() - s) + " seconds");
}, 500);
while(true) {
if(new Date().getSeconds() - s >= 2) {
console.log("Good, looped for 2 seconds");
break;
}
}
Intervalos de zero segundos
O intervalo zero não significa, necessariamente, que o callback será disparado após zero milissegundos. Chamar setTimeout
com um intervalo de 0 (zero) milissegundos não executa a função do callback após dado intervalo.
A execução depende do número de mensagens em espera na fila. No exemplo abaixo, a mensagem ''this is just a message'' será escrita no console antes que a mensagem do callback seja processada, isso acontece porque o intervalo definido na função indica o tempo mínimo necessário para que a aplicação processe a requisição, mas não é um tempo garantido.
Basicamente, setTimeout
precisa esperar que todo o código das mensagens enfileiradas seja concluído, mesmo que você tenha especificado um tempo limite específico para o seu setTimeout
.
(function() {
console.log('this is the start');
setTimeout(function cb() {
console.log('Callback 1: this is a msg from call back');
}); // tem um valor de tempo padrão de 0
console.log('this is just a message');
setTimeout(function cb1() {
console.log('Callback 2: this is a msg from call back');
}, 0);
console.log('this is the end');
})();
// "this is the start"
// "this is just a message"
// "this is the end"
// "Callback 1: this is a msg from call back"
// "Callback 2: this is a msg from call back"
Múltiplos runtimes comunicando-se em conjunto
Um web worker ou um iframe
com uma diferente origem (cross-origin) tem as suas próprias pilhas, heaps e filas de messagens. Dois runtimes distintos só podem se comunicar por meio do envio de mensagens, via método postMessage
. Este método adiciona uma mensagem ao outro runtime, se este escutar os eventos de message
.
Sem bloqueio
Uma propriedade muito interessante do modelo "event loop", é que o JavaScript, ao contrário de muitas outras linguagens, nunca bloqueia. A manipulação de E/S é tipicamente realizada através de eventos e callbacks, portanto, quando uma aplicação está esperando por um retorno de uma consulta do IndexedDB ou o retorno de uma requisição XHR, este ainda pode processar outras coisas, como as ações do usuário.
Exceções de legado existem, como por exemplo, alert
ou XHR síncrono, mas é considerado uma boa prática evitá-los. Tome cuidado, exceções a exceção existem (mas geralmente são, mais do que qualquer coisa, bugs de implementação).
Especificações
Especificação | Status | Comentário |
---|---|---|
HTML Living Standard The definition of 'Event loops' in that specification. |
Padrão em tempo real | |
Node.js Event Loop | Living Standard |