Nesse texto, vamos compreender um pouco mais sobre um paradigma importante quando estamos programando em JavaScript e, além disso, entender uma das principais características que levaram ao sucesso da linguagem: o seu modelo de concorrência.
Assincronia em JavaScript 🙝
Programação assíncrona é um paradigma de programação onde o código pode não seguir o fluxo de execução “padrão”. Além disso, um código assíncrono pode tratar de interrupções externas ao programa, tais como a chegada de um evento, sinal, temporizador, entre outras.
Em JavaScript, podemos escrever código assíncrono de forma fácil utilizando a função setTimeout
, que recebe dois parâmetros: uma função a ser executada e uma quantidade de milissegundos que indica o tempo de espera para iniciar a execução dessa função. Veja o exemplo abaixo:
function foo() {
setTimeout(function () {
console.log('without one.');
}, 1000);
setTimeout(function () {
console.log('fully dressed');
}, 0);
console.log('You know');
}
console.log('Smile, my dear!');
foo();
console.log('you are not');
A saída do código acima será escrita como:
Smile, my dear! You know you are not fully dressed without one.
Apesar dos logs serem colocados fora de ordem, alguns foram colocados em funções que foram passadas para o setTimeout
e que serão executadas somente após o tempo de espera for esgotado. Vamos ver outro exemplo um pouco mais complexo. Considere o código abaixo:
function asyncCountTo(x) {
for (var i = 0; i < x; i++) {
setTimeout(
function () {
console.log(i);
},
(x - i) * 1000,
);
}
}
asyncCountTo(4);
Qual é a saída do código acima? Temos um laço que dispara várias funções por meio do setTimeout
e que imprimem a variável i
. Por maior que seja a surpresa, o código acima exibe como saída:
4 4 4 4
Por quê? A razão desse comportamento se deve à closure associada a função passada para o setTimeout
. A variável i
é compartilhada por todas as closures
e, por isso, tem esse comportamento. O laço continua sendo executado e a variável i
continua sendo incrementada até chegue ao valor quatro, que é quando a condição do laço falha. Só então os callbacks do setTimeout
são executados.
No entanto, você percebeu que mesmo quando o tempo de espera for zero, a função não é executada imediatamente? Bem, aqui estamos de frente com um dos efeitos do modelo de concorrência do JavaScript e que vamos entrar em detalhes a partir de agora.
Programação Orientada a Eventos 🙝
O JavaScript é uma linguagem que foi inicialmente desenvolvida para adicionar funcionalidades as páginas HTML do navegador Netscape. É uma linguagem multiparadigma que suporta programação procedural, orientada a objetos e funcional, mas, foi especialmente desenvolvida para atender a programação orientada a eventos.
A programação orientada a eventos ajusta o fluxo do programa de acordo com eventos, ou seja, interrupções que podem ser feitas a qualquer momento e necessitam de “reações” por parte do programa. É um paradigma muito utilizado para o desenvolvimento de drivers e sensores de microcontroladores. A programação orientada a eventos também predomina no desenvolvimento de aplicações com GUI, pois requerem que a aplicação reaja de acordo com a interação do usuário. Não é por coincidência que ela seria utilizada em “uma linguagem que quer adicionar algumas funcionalidades em páginas HTML”, certo?
Em uma aplicação orientada a eventos, normalmente temos um Laço de Eventos — comumente chamado de event loop — que aguarda os eventos acontecerem para chamar funções que foram designadas à responder um evento específico. E é o comportamento desse laço o qual vamos detalhar daqui em diante.
O Event Loop do JavaScript 🙝
Toda máquina de execução JavaScript possui um event loop único que captura os eventos disparados pelo usuário ou pelo ambiente onde a máquina está alocada. Se um evento capturado pelo event loop possuir algum callback associado — uma função que deve ser executada quando o evento ocorrer — então uma tarefa será enfileirada na fila de tarefas que irá iniciar a execução do callback pelo motor de execução JavaScript.
A fila de tarefas, também chamada de job queue, é uma estrutura de fila que armazena referência para funções que devem ser executadas. De forma geral, o motor de execução do JavaScript possui um algoritmo bastante simples:
- Aguarde a fila ter tarefas;
- Execute a primeira tarefa da fila até o fim, isto é, até que a pilha de execução esteja vazia.
- Retorne ao passo 1.
Na verdade, a função setTimeout
que vimos anteriormente não “executa uma função após uma quantidade de tempo”, mas, adiciona no event loop a função como um callback para um sinal de um temporizador que será disparado após a quantidade de tempo do segunto parâmetro. Existem outras formas de adicionar tarefas a fila, entre elas:
- Adicionar listeners de eventos com
addEventListener
; - Utilizando
setInterval
oupostMessage
; - Respostas de requisições HTTP utilizando as APIs
XMLHttpRequest
oufetch
dos navegadores.
O event loop é um modelo de concorrência não preemptivo. Não é possível interromper a execução da função corrente e retornar posteriormente. Somente uma função é executada por vez. Uma vez que a computação de uma função é iniciada, ela não é mais interrompida. Assim, não há paralelismo de execução das funções no event loop.
É por isso que, mesmo quando colocado no setTimeout
com tempo de espera zero, o callback não é executado imediatamente. Ainda é necessário terminar a execução da função atual e, somente então, o callback é executado. Isso explica o comportamendo da saída do código do exemplo anterior. Na realidade, o setTimeout
não garante que a função vai ser executada após o período, mas sim que sua tarefa será enfileirada.
Outro efeito colateral desse modelo de concorrência é que, se alguma tarefa demanda muita computação, ela inevitavelmente vai bloquear a fila de tarefas por muito tempo e, assim, nenhuma outra tarefa poderá ser iniciada. Se a fila de tarefas está bloqueada, os callbacks dos eventos das ações do usuário podem não ser respondidos em tempo adequado e a GUI apresenta estar “travada”, por exemplo.
Indo além 🙝
Agora que você já entende o modelo de concorrência, alguns questionamentos podem estar perambulando pela sua cabeça:
Mas, quando fazemos uma requisição HTTP utilizando a API fetch, o usuário ainda é capaz de interagir com a tela mesmo que a requisição demore vários segundos para ser concluída… Como isso acontece se o event loop está bloqueado?
Diferentes máquinas virtuais JavaScript implementam algumas funcionalidades em fluxos de execução paralelos ao event loop para não bloquear a fila de tarefas por muito tempo. Normalmente tais funcionalidades estão relacionadas a operações de entrada e saída que, comumente, são operações “lentas”. Alguns exemplos de operações de entrada e saída que normalmente são executadas paralelamente ao event loop incluem: leitura e escrita de arquivos, requisições de rede, stream de multimídia, acesso ao banco de dados, entre outros.
Essa publicação apresentou uma versão bastante simplificada de como o event loop do JavaScript funciona. Ainda há vários pontos que foram deixados de lado para o bem da sanidade da explicação. No entanto, acredito que você tenha conseguido entender o funcionamento das máquinas virtuais JavaScript e compreendido o modelo de concorrência da linguagem e as consequências que surgem a partir do seu uso. Caso você tenha interesse em uma visão mais completa, recomendo ler mais sobre:
- A fila de microtarefas e as promises;
- A instanciação de event loops paralelos com Workers;
- As etapas de apresentação nos navegadores.