Programação assíncrona de JavaScript e Callbacks

Assincronicidade em Linguagens de Programação

Computadores são assíncronos por definição.

Assíncrono significa que as coisas podem acontecer independemente do fluxo principal do programa.

Nos computadores de uso doméstico atuais, cada programa roda por um intervalo de tempo específico e, em seguida, interrompe sua execução para permitir que outro programa continue sua execução. Isso roda em um ciclo tão rápido que é impossível de notar. Nós pensamos que nossos computadores rodam diversos programass simultaneamente, mas isso é uma ilusão (exceto em máquinas multiprocessadas).

Programas usam internamente sinais de interrupção (interrupts), que são emitidos ao processador para ganharem a atenção do sistema.

Não irei muito a fundo no conceito, mas só tenha em mente que é normal para programas serem assíncronos e pararem a execução até que precisem de atenção, permitindo ao computador executar outras coisas nesse meio tempo. Quando um programa está esperando por uma resposta da rede, ele não pode parar o processador até que a requisição finalize.

Normalmente, linguages de programação são síncronas e algumas fornecem maneiras de gerenciar assincronicidade na própria linguagem ou por meio de bibliotecas. C, Java, C#, PHP, Go, Ruby, Swift, e Python são todas síncronas por padrão. Algumas delas gerenciam assíncronismo usando threads, gerando um novo processo.

JavaScript

JavaScript é síncrono por padrão e roda em uma única thread. Isso significa que o código não pode criar novas threads e rodar em paralelo.

Linhas de código são executadas em séries, uma após a outra, por exemplo:

const a = 1
const b = 2
const c = a * b
console.log(c)
doSomething()

Mas o JavaScript nasceu dentro dos browsers, sua função principal, desde o começo, era responder às interações do usuário, como onClick, onMouseOver, onChange, onSubmit e etc. Como isso pode ser feito com um modelo programático síncrono?

A resposta era o ambiente. O browser dá um jeito nisso fornecendo um conjunto de APIs que podem lidar com esses tipos de funcionalidades.

Mais recentemente, o Node.js introduziu um ambiente de I/O não bloqueante para extender esse conceito a acesso de arquivos, chamadas de rede e etc.

Callbacks

Não tem como você saber quando um usuário vai clicar em um botão. Então, você define uma função para lidar com o evento de click. Essa função será chamada quando o evento for acionado:

document.getElementById('button').addEventListener('click', () => {
// item clicado
})

Isso também é chamado de callback.

Uma callback é uma simples função que é passada como valor para outra função, e só será executada quando o evento ocorrer. Nós podemos fazer isso por que o JavaScript têm funções de "primeira classe", que podem ser atribuidas à variáveis e passadas para outras funções chamadas de higher-order functions (funções de ordem superior).

É comum envolver todo código do cliente em um event listener do tipo load no objeto window, que roda só roda a função callback quando a página está pronta:

window.addEventListener('load', () => {
// window carregou
// faça o que quiser
})

Callbacks são usadas em todo lugar, não só em eventos do DOM.

Um exemplo comum é utilizando temporizadores:

setTimeout(() => {
// roda após 2 segundos
}, 2000)

Requisições XHR também aceitam uma callback, nesse exemplo ao passar uma função para uma propriedade que será chamada quando um evento em particular ocorrer (nesse caso, o estado da requisição mudar):

const xhr = new XMLHttpRequest()
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
xhr.status === 200 ? console.log(xhr.responseText) : console.error('error')
}
}
xhr.open('GET', 'https://yoursite.com')
xhr.send()

Tratando erros em callbacks

Como você trata erros em callbacks? Uma estratégia muito comum é usar o que o Node.js adotou: o primeiro parâmetro em qualquer função callback é o objeto de erro: error-first callbacks

Se não há erro, o objeto é nulo (null). Se há um erro, ele contêm alguma descrição do erro e outras informações.

fs.readFile('/file.json', (err, data) => {
if (err !== null) {
// tratando erro
console.log(err)
return
}
// sem erros, processe o data
console.log(data)
})

O problema com callbacks

Callbacks são ótimas para casos simples!

Entretanto, toda callback adiciona um nível de aninhamento, e quando você têm muitas callbacks, o código começa a se complicar rapidamente:

window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// seu código aqui
})
}, 2000)
})
})

Esse é só um exemplo simples com 4 níveis de aninhamento, mas eu já vi códigos com muito mais níveis e não é nem um pouco divertido.

Como resolver isso?

Alternativas à callbacks

A partir do ES6, o JavaScript introduziu várias funcionalidades que nos ajudam a lidar com código assíncrono sem o uso de callbacks: Promises (ES6) e Async/Await (ES2017).