Usamos métodos de navegador em exemplos aqui
Para demonstrar o uso de callbacks, promessas e outros conceitos abstratos, usaremos alguns métodos de navegador: especificamente, carregar scripts e realizar manipulações simples de documentos.
Se você não estiver familiarizado com esses métodos e seu uso nos exemplos for confuso, você pode querer ler alguns capítulos da próxima parte do tutorial.
Embora, tentaremos deixar as coisas claras de qualquer maneira. Não haverá nada realmente complexo em termos de navegador.
Muitas funções são fornecidas por ambientes host JavaScript que permitem agendar ações assíncronas . Ou seja, ações que iniciamos agora, mas que terminam depois.
Por exemplo, uma dessas funções é a função setTimeout
.
Existem outros exemplos reais de ações assíncronas, por exemplo, carregamento de scripts e módulos (abordaremos isso em capítulos posteriores).
Dê uma olhada na função loadScript(src)
, que carrega um script com o src
fornecido:
função carregarScript(src) { // cria uma tag <script> e a anexa à página // isso faz com que o script com o src fornecido comece a ser carregado e executado quando concluído deixe script = document.createElement('script'); script.src=src; documento.head.append(script); }
Ele insere no documento uma nova tag <script src="…">
criada dinamicamente com o src
fornecido. O navegador começa a carregá-lo automaticamente e é executado quando concluído.
Podemos usar esta função assim:
// carrega e executa o script no caminho fornecido loadScript('/meu/script.js');
O script é executado “de forma assíncrona”, pois começa a carregar agora, mas é executado mais tarde, quando a função já tiver finalizado.
Se houver algum código abaixo loadScript(…)
, ele não esperará até que o carregamento do script termine.
loadScript('/meu/script.js'); // o código abaixo loadScript // não espera o carregamento do script terminar // ...
Digamos que precisamos usar o novo script assim que ele for carregado. Ele declara novas funções e queremos executá-las.
Mas se fizermos isso imediatamente após a chamada loadScript(…)
, isso não funcionaria:
loadScript('/meu/script.js'); // o script tem "função newFunction() {…}" novaFunção(); // essa função não existe!
Naturalmente, o navegador provavelmente não teve tempo de carregar o script. No momento, a função loadScript
não fornece uma maneira de rastrear a conclusão do carregamento. O script é carregado e eventualmente executado, só isso. Mas gostaríamos de saber quando isso acontecer, para usar novas funções e variáveis desse script.
Vamos adicionar uma função callback
como segundo argumento ao loadScript
que deve ser executado quando o script for carregado:
function loadScript(src, retorno de chamada) { deixe script = document.createElement('script'); script.src=src; script.onload = () => retorno de chamada (script); documento.head.append(script); }
O evento onload
está descrito no artigo Carregamento de recursos: onload e onerror, basicamente executa uma função após o script ser carregado e executado.
Agora, se quisermos chamar novas funções do script, devemos escrever isso no retorno de chamada:
loadScript('/my/script.js', function() { // o retorno de chamada é executado após o carregamento do script novaFunção(); //então agora funciona ... });
A ideia é essa: o segundo argumento é uma função (geralmente anônima) que é executada quando a ação é concluída.
Aqui está um exemplo executável com um script real:
function loadScript(src, retorno de chamada) { deixe script = document.createElement('script'); script.src=src; script.onload = () => retorno de chamada (script); documento.head.append(script); } loadScript('https://cdnjs.cloudflare.com/ajax/libs/lodash.js/3.2.0/lodash.js', script => { alert(`Legal, o script ${script.src} está carregado`); alerta(_); // _ é uma função declarada no script carregado });
Isso é chamado de estilo de programação assíncrona “baseado em retorno de chamada”. Uma função que faz algo de forma assíncrona deve fornecer um argumento callback
onde colocamos a função para ser executada após sua conclusão.
Aqui fizemos isso em loadScript
, mas é claro que é uma abordagem geral.
Como podemos carregar dois scripts sequencialmente: o primeiro e o segundo depois dele?
A solução natural seria colocar a segunda chamada loadScript
dentro do callback, assim:
loadScript('/my/script.js', function(script) { alert(`Legal, o ${script.src} está carregado, vamos carregar mais um`); loadScript('/my/script2.js', function(script) { alert(`Legal, o segundo script está carregado`); }); });
Após a conclusão do loadScript
externo, o retorno de chamada inicia o interno.
E se quisermos mais um roteiro…?
loadScript('/my/script.js', function(script) { loadScript('/my/script2.js', function(script) { loadScript('/my/script3.js', function(script) { // ...continua após todos os scripts serem carregados }); }); });
Portanto, cada nova ação está dentro de um retorno de chamada. Isso é bom para poucas ações, mas não é bom para muitas, então veremos outras variantes em breve.
Nos exemplos acima não consideramos erros. E se o carregamento do script falhar? Nosso retorno de chamada deve ser capaz de reagir a isso.
Aqui está uma versão melhorada do loadScript
que rastreia erros de carregamento:
function loadScript(src, retorno de chamada) { deixe script = document.createElement('script'); script.src=src; script.onload = () => retorno de chamada (nulo, script); script.onerror = () => callback(new Error(`Erro de carregamento de script para ${src}`)); documento.head.append(script); }
Ele chama callback(null, script)
para carregamento bem-sucedido e callback(error)
caso contrário.
O uso:
loadScript('/my/script.js', function(erro, script) { se (erro) { // trata o erro } outro { //script carregado com sucesso } });
Mais uma vez, a receita que usamos para loadScript
é bastante comum. É chamado de estilo “retorno de chamada com erro primeiro”.
A convenção é:
O primeiro argumento do callback
é reservado para um erro, caso ocorra. Então callback(err)
é chamado.
O segundo argumento (e os próximos, se necessário) são para o resultado bem-sucedido. Então callback(null, result1, result2…)
é chamado.
Portanto, a função callback
única é usada tanto para relatar erros quanto para retornar resultados.
À primeira vista, parece uma abordagem viável para codificação assíncrona. E de fato é. Para uma ou talvez duas chamadas aninhadas, parece bom.
Mas para múltiplas ações assíncronas que seguem uma após a outra, teremos um código como este:
loadScript('1.js', function(erro, script) { se (erro) { handleError(erro); } outro { // ... loadScript('2.js', function(erro, script) { se (erro) { handleError(erro); } outro { // ... loadScript('3.js', function(erro, script) { se (erro) { handleError(erro); } outro { // ...continua após todos os scripts serem carregados (*) } }); } }); } });
No código acima:
Carregamos 1.js
, então se não houver erro…
Carregamos 2.js
e se não houver erro…
Carregamos 3.js
e, se não houver erro, faça outra coisa (*)
.
À medida que as chamadas se tornam mais aninhadas, o código se torna mais profundo e cada vez mais difícil de gerenciar, especialmente se tivermos código real em vez de ...
que pode incluir mais loops, instruções condicionais e assim por diante.
Isso às vezes é chamado de “inferno de retorno de chamada” ou “pirâmide da destruição”.
A “pirâmide” de chamadas aninhadas cresce para a direita a cada ação assíncrona. Logo fica fora de controle.
Portanto, essa forma de codificação não é muito boa.
Podemos tentar aliviar o problema tornando cada ação uma função independente, como esta:
loadScript('1.js', passo1); function etapa 1(erro, script) { se (erro) { handleError(erro); } outro { // ... loadScript('2.js', passo2); } } function step2(erro, script) { se (erro) { handleError(erro); } outro { // ... loadScript('3.js', passo3); } } function step3(erro, script) { se (erro) { handleError(erro); } outro { // ...continua após todos os scripts serem carregados (*) } }
Ver? Ele faz a mesma coisa, e não há aninhamento profundo agora porque transformamos cada ação em uma função separada de nível superior.
Funciona, mas o código parece uma planilha rasgada. É difícil de ler e você provavelmente notou que é preciso pular os olhos entre as peças enquanto o lê. Isso é inconveniente, especialmente se o leitor não estiver familiarizado com o código e não souber para onde olhar.
Além disso, as funções denominadas step*
são todas de uso único, criadas apenas para evitar a “pirâmide da destruição”. Ninguém irá reutilizá-los fora da cadeia de ação. Portanto, há um pouco de confusão de namespace aqui.
Gostaríamos de ter algo melhor.
Felizmente, existem outras maneiras de evitar essas pirâmides. Uma das melhores maneiras é usar “promessas”, descritas no próximo capítulo.