O fluxo de execução de um programa é determinado pela ordem em que suas instruções são executadas. Tradicionalmente a execução é sequencial e segue a ordem em que as instruções aparecem no código fonte do programa.
Existem instruções especiais que podem guiar o fluxo de execução, seja pela imposição de uma decisão, repetição ou pulo. No JavaScript, temos como exemplo if
, for
, while
, try catch
, return
, dentre outros (como break
e continue
que podem ser usados de um jeito peculiar).
Como a maioria das linguagens de programação, o JavaScript foi projetado para funcionar com um único fluxo de execução. Apenas uma instrução do seu código será processada em um determinado instante. No ambiente do navegador, seu código JavaScript ainda terá que disputar vaga com o rendering (layout e paint) da página. Isto significa que o navegador não conseguirá posicionar ou desenhar um elemento na tela ao mesmo tempo que executa JavaScript e vice-versa.
Fluxos assíncronos
Mesmo que projetado para ter um único fluxo de execução, algumas tarefas do seu programa JavaScript poderão ser executadas sem interferir em nada o fluxo principal de execução de código. Essas tarefas são conhecidas como fluxos assíncronos. Você pode disparar uma série dessas tarefas sem precisar esperar que cada uma se complete para prosseguir. Nos computadores atuais, que possuem mais de um núcleo de processamento, algumas tarefas do escopo do seu programa poderão inclusive ser executadas no mesmo instante do seu código.
Isso pode parecer um pouco complicado e um exemplo pode ajudar. Toda vez que você dispara uma requisição Ajax, a comunicação com o servidor e o recebimento dos dados ocorre em paralelo a execução de outras linhas do seu programa. Quando tudo estiver pronto, você será avisado no mesmo fluxo de execução que disparou a requisição, e então poderá usar sua resposta.
Os navegadores possuem uma série de APIs e funções que executam em fluxos assíncronos. Abaixo a lista das mais relevantes:
Timers
As funções setTimeout
e setInterval
criam contadores que irão avisar seu programa que determinado período de tempo se passou de forma assíncrona.
Comunicação com servidor
As APIs para comunicação Ajax e outras que implementam os vários protocolos de comunicação que fazem parte da Web são assíncronas. Web Sockets e Server Sent Events criam conexões permanentes com o servidor no navegador, retornando dados ao longo do tempo para nosso código.
Eventos do DOM
Através dos eventos do DOM é possível perceber interações do usuário e monitorar mudanças de estado do documento. Como o usuário pode reagir a qualquer instante, o tratamento obrigatoriamente precisa se dar de forma assíncrona. Não é viável bloquear o fluxo de execução principal a espera de um clique.
Mensagens
A função window.postMessage
e o tratador de evento de recebimento de mensagem, permitem, respectivamente, enviar e receber mensagens de forma assíncrona entre diferentes páginas (ou iframes) abertas no navegador.
Mutation Observers
Os Mutation Observers permitem assistir a mudanças no documento. Através destes, é possível ser avisado a cada alteração de atributo, criação, remoção ou alteração de elementos da página. Seu comportamento e justificativa de existência é a mesma dos eventos do DOM.
Callbacks e closures
As chamadas de função que executam em um fluxo assíncrono fazem uso de callbacks. Callbacks são porções de código que serão executados no futuro, em um tempo conveniente.
O JavaScript interage com os fluxos assíncronos através de troca de mensagens. Sempre que uma tarefa assíncrona entra em ação, a callback é registrada e fica a espera de uma mensagem para ser executada. Existe então um loop de eventos que aguarda por mensagens emitidas por timers, requisições, eventos do DOM e outras chamadas assíncronas.
As mensagens, quando recebidas, são mantidas em uma fila e disparam a execução da callback, uma por vez. É importante notar que duas callbacks não são disparadas no mesmo instante. Existe um único fluxo de execução JavaScript nos navegadores e as callbacks são executadas nele também.
Tudo é executado em paralelo, exceto o seu código – Mikito Takada
As callbacks possuem acesso às variáveis do escopo em que foram definidas, por isto são chamadas closures. Apesar disto, callbacks não são executadas no mesmo escopo em que definidas.
Quebrando o fluxo de execução
Quando escrevemos código para adicionar interatividade a uma página, existem algumas situações que pode ser interessante que o fluxo de execução não seja contínuo. Como vimos anteriormente, a execução de nosso código é compartilhada com o rendering da página. Qualquer execução que tome um pouco mais de tempo, irá bloquear a interface e transmitir uma impressão de lentidão para o usuário.
Um artifício que pode ser usado para permitir que o rendering não seja prejudicado é quebrar a execução de uma tarefa em fatias. Para isto, será preciso dividir a tarefa em callbacks e fazer uso de recursos que criem fluxos assíncronos. A mais simples das funções que conhecemos já pode ajudar: setTimeout(callback, 0)
.
A cada chamada de setTimeout
um fluxo assíncrono será criado para gerenciar um temporizador. Assim que as demais instruções forem executadas, o fluxo principal será interrompido. O navegador poderá então utilizar seus recursos para fazer tarefas relacionadas ao rendering, garantindo assim uma interface fluída para o usuário. Quando tais tarefas terminarem, o temporizador de zero segundos já deverá ter disparado uma mensagem a ser atendida pelo loop de eventos do JavaScript. A callback será disparada.
Usar setTimout
, apesar de ser um truque baixo, irá funcionar bem para tarefas simples. Porém, existe uma limitação em sua especificação. Sempre que callbacks disparadas por um temporizador criarem outro temporizador, este terá tempo mínimo de 4 milissegundos. Existe uma especificação em andamento para definir a função setImmediate
. E para jogos e animações, melhor mesmo é usar a função requestAnimationFrame
que cria um novo fluxo de execução logo após o rendering.
Problemas das callbacks
O único mecanismo que o JavaScript oferece para gerenciar fluxos de execução assíncronos são as callbacks. É provável que no futuro tenhamos os Generators e Async Functions. Mas por enquanto todas as APIs recebem apenas callbacks.
O problema é que alguns códigos que utilizam callbacks são frequentemente mal compreendidos. O código abaixo, apesar de parecer, não irá mostrar um contador regressivo para o usuário. Como já mencionado, o que as closures enxergam são variáveis e não valores.
for (var i=0; i<4; i++) {
setTimeout(function () {
alert(4-i)
}, i*100)
}
A solução para o código acima é utilizar o .bind
para armazenar os diferentes valores de i
. Uma solução melhor ainda é reescrever este algoritmo seguindo um estilo mais funcional:
Array.apply(null, Array(4)).forEach(function (undef, index) {
setTimeout(function () {
alert(4-index)
}, index*100)
})
Outra confusão comum envolve excessões. Observe o código a seguir e tente prever seu comportamento:
try {
setTimeout(function () {
throw "Exception"
}, 100)
}
catch (e) {}
A exceção emitida pela callback não será coletada e explodirá direto para o usuário. A callback, quando executada, não estará mais sendo assistida pelo bloco de tratamento de exceção. Um novo bloco de tratamento deve ser criado no interior da função.
O valor que o this
irá representar no interior da callback algumas vezes gera dúvidas. Esqueça! Geralmente é uma má ideia utilizar o this
nestes casos. Se for preciso, é bom recorrer ao .bind
da callback com o valor que se espera que o this
represente. Logo será possível utilizar também Arrow Functions para contornar este problema.
Por fim, há desenvolvedores que argumentam contra callbacks pelo uso destas resultar em um código confuso e com alto acoplamento. O site Callback Hell ensina de forma prática como evitar o temido callback hell (sic).
Apesar de todos estes problemas e confusões, as callbacks são simples e poderosas.
Espero que tenha aproveitado a leitura. No próximo artigo da série, veremos algumas limitações reais das callbacks. Seremos apresentados às promises, que irão garantir uma escrita de código melhor em determinadas circunstâncias. Até lá. O próximo artigo da série que fala sobre _promises _já saiu.