Os computadores são assíncronos por design.
Assíncrono significa que as coisas podem acontecer indepentemente do fluxo do programa principal.
Nos computadores de consumo atuais, cada programa é executado por um intervalo de tempo específico e, em seguida, intrompe sua execução para permitir que outro programa continue sua execução. Essa coisa funciona em um ciclo tão rápido que é impossível perceber. Achamos que nossos computadores executam muitos programas simultaneamente, mas isso é uma ilusão (exceto em máquinas multiprocessadoras).
Os programas usam internamente as interrupções, um sinal que é emitido ao processador para chamar a atenção do sistema.
Não vamos entrar em detalhes agora, mas apenas tenha em mente que é normal que os programas sejam assíncronos e interrompam sua execução até que precisem de atenção, permitindo que o computador execute 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 solicitação termine.
Normalmente, as linguagens de programação são síncronas e algumas fornecem uma maneira de gerenciar a assincronia na linguagem ou por meio de bibliotecas. C, Java, C#, PHP, Go, Ruby, Swift e Python são todos síncronos por padrão. Alguns deles lidam com operações assíncronas usando threads, gerando um novo processo.
O JavaScript é síncrono por padrão e é de encadeamento único. Isso significa que o código não pode criar novos threads e ser executado em paralelo.
As linhas de código são executados em série, 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 do navegador, seu principal trabalho, no início, era responder às ações do usuário, como onClick
, onMouseOver
, onChange
, onSubmit
e assim por diante. Como poderia fazer isso com um modelo de programação síncrona?
A resposta estava em seu ambiente. O navegador fornece uma maneira de fazer isso fornecendo um conjunto de APIs que podem lidar com esse tipo de funcionalidade.
Mais recentemente, o Node.js introduziu um ambiente de I/O sem bloqueio para estender esse conceito de acesso a arquivos, chamadas de redes e assim por diante.
Você não pode saber quando um usuário vai clicar em um botão. Assim, você define um manipulador de eventos para o evento de clique. Este manipulador de eventos aceita uma função, que será chamada quando o evento for adicionado:
document.getElementById('button').addEventListener('click', () => {
// item clicado
});
Este é o chamado callback.
Um callback é uma função simples que é passado como valor para outra função e só será executada quando o evento acontecer. Podemos fazer isso porque o JavaScript tem funções de primeira classe, que podem ser atribuídas a variáveis e passadas para outras funções (chamadas funções de ordem superior).
É comum envolver todo o código do seu cliente em um evento chamado load
no objeto window
, que executa a função de retorno de chamada somente quando a página está pronta:
window.addEventListener('load', () => {
// janela carregada
// O que você quer
});
Callbacks são usados em todos os lugares, não apenas em eventos DOM.
Um exemplo comum é usados temporizadores:
setTimeout(() => {
// executa depois de 2 segundos
}, 2000);
As solicitações XHR também aceitam um callback, neste exemplo, atribuindo uma função a uma propriedade que será chamado quando um determinado evento ocorrer (neste caso, o estado da solicitação muda):
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();
Como você lida com erros com callbacks? Uma estratégia muito comum é usar o que o Node.js adotou: o primeiro parâmetro em qualquer função de callback é o objeto error: error-first callbacks.
Se não houver erro, o objeto é null
. Se houver um erro, ele contém alguma descrição do erro e outras informações.
const fs = require('fs');
fs.readFile('/file.json', (err, data) => {
if (err) {
// manipulador de erros
console.log(err);
return;
}
// sem erros, processa os dados
console.log(data);
});
Os callbacks são ótimos para casos simples!
No entanto, cada callback adiciona um nível de aninhamento e, quando você tem muitos callbacks, o código começa a ficar complicado muito rapidamente:
window.addEventListener('load', () => {
document.getElementById('button').addEventListener('click', () => {
setTimeout(() => {
items.forEach(item => {
// seu código aqui
});
}, 2000);
});
});
Este é apenas um código simples de 4 níveis, mas já vi muitos mais níveis de aninhamento e não é divertido.
Como solucionamos isso?
A partir do ES6, o JavaScript introduziu vários recursos que nos ajudam com código assíncrono que não envolve o uso de callbacks: Promises (ES6) e Async/Awit (ES2017).