28 de outubro de 2022 • 7 min de leitura
Funções callback em JavaScript
Entenda o que é uma função callback no javascript
Como o JavaScript funciona?
Ele executa em uma ordem de cima para baixo, uma sequência de instruções que chamamos de Algoritmo. Imagine que essa sequência de instruções é similar a receita de um bolo, basta você seguir passo a passo dessa receita na ordem correta para que o seu bolo fique pronto.
1. Misturar ovos, manteiga e açucar.
2. Acrescentar farinha de trigo, fermento, leite e a massa do bolo.
3. Continuar misturando tudo por 5 minutos.
4. Pré aquecer o forno em 180ºC por 5 minutos.
5. Colocar a mistura pronta em uma forma.
6. Colocar a forma no forno e deixar por 40 minutos.
exemplo de um algoritmo javascript:
function firstPokemon() {
console.log('Charmander');
}
function secondPokemon() {
console.log('Squirtle');
}
firstPokemon();
secondPokemon();
OUTPUT:
Charmander
Squirtle
Entendendo Callback e sua Semântica
Agora vamos demonstrar outro exemplo dessa vez utilizando a função setTimeout()
para que o primeiro pokemon demore 3 segundos para ser exibido.
function firstPokemon() {
console.log('Charmander');
}
setTimeout(firstPokemon(), 3000);
OUTPUT:
Charmander
node:internal/validators:233
throw new ERR_INVALID_CALLBACK(callback);
^
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
at setTimeout (node:timers:141:3)
at Object.<anonymous> (/Users/jefferson/Development/PROJECTS/blog-soaresdev/tempCodeRunnerFile.javascript:5:1)
at Module._compile (node:internal/modules/cjs/loader:1126:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
at Module.load (node:internal/modules/cjs/loader:1004:32)
at Function.Module._load (node:internal/modules/cjs/loader:839:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47 {
code: 'ERR_INVALID_CALLBACK'
}
Ao executar o algoritmo causamos este erro acima, quero que você observe duas coisas:
- A função
firstPokemon
é executada automaticamente, sem a espera dos 3 segundos - A função
setTimeout
lança um erro decallback
inválida.
Ao executar firstPokemon()
com os parênteses no final, faz com que essa função seja invocada, ou seja, ordenamos a execução imediata da função firstPokemon()
. E quando o setTimeout
procurou o que havia no primeiro parâmetro, não encontrou nada (recebeu undefined), já que firstPokemon
já havia sido executada.
Para entender completamente, vamos para um exemplo parecido, mas dessa vez retirando os parênteses no final de firstPokemon()
.
function firstPokemon() {
console.log('Charmander');
}
setTimeout(firstPokemon, 3000);
// OUTPUT:
// Charmander
Perfeito! Podemos entender que, devemos passar a função firstPokemon
como argumento sem os parênteses, pois assim é a semântica correta, e que as famosas Callbacks
não são funções invocada diretamente, e sim sendo passada como parâmetro para que outra função chame ela. Neste exemplo a função setTimeout
chama (CALL) novamente (BACK).
Vamos seguir com outro exemplo, iremos passar uma função como Callback
para que a função receiveCallback
possa chamar (e executar) essa função dentro.
function receiveCallback(callbackReceived) {
const aleatoryNumber = 33;
callbackReceived(aleatoryNumber);
}
function printValue(valueToPrint) {
console.log(valueToPrint);
}
receiveCallback(printValue);
// OUTPUT:
// 33
- A função
printValue
será responsável somente por imprimir o valor que recebe como parâmetro. - A função
receiveCallback
será invocada recebendoprintValue
comoCallback
e assim seguirá. - Invocar a
Callback
chamadaprintValue
recebida como parâmetro que consequentemente imprimirá no console o número 33.
JavaScript assíncrono com Callbacks
Na etapa anterior aprendemos como passar funções callbacks
, agora vamos aprender
a verdadeira vantagem, que é utilizar callbacks
no javascript assíncrono.
No exemplo a seguir vamos reutilizar as funções do início desse post,
O próximo exemplo reutiliza as duas funções do início do artigo firstPokemon()
e secondPokemon()
. Na função firstPokemon()
configuramos uma espera de 3
segundos para execução e logo em seguida iremos chamar a função secondPokemon()
.
function firstPokemon() {
console.log('Charmander');
}
function secondPokemon() {
console.log('Squirtle');
}
setTimeout(firstPokemon, 3000);
segundaMensagem();
// OUTPUT:
// Squirtle
// ...3 segundos depois
// Charmander
O que houve? Se o código é executado em ordem de cima para baixo, por que o JavaScript não esperou que Charmander
fosse exibida antes de Squirtle
?
Neste caso o JavaScript leu o código corretamente, executou a ação dos 3 segundos para exibir a mensagem e continuou a execução de forma síncrona do restante do código. Ou seja, ele leu setTimeout, mandou a execução acontecer e continuou a ler a próxima linha de código que neste caso foi secondPokemon()
.
Mas esse não é o output que queríamos, certo? Queríamos que secondPokemon()
fosse executada somente após firstPokemon()
. E se fizéssemos uma modificação? Vamos incluir secondPokemon()
como callback
de firstPokemon()
e fazer com que essa callback
seja executada 2 segundos depois da execução da primeira.
function firstPokemon(callback) {
console.log('Charmander');
setTimeout(callback, 2000);
}
function secondPokemon() {
console.log('Squirtle');
}
setTimeout(firstPokemon(secondPokemon), 3000);
OUTPUT:
Charmander
node:internal/validators:233
throw new ERR_INVALID_CALLBACK(callback);
^
TypeError [ERR_INVALID_CALLBACK]: Callback must be a function. Received undefined
at setTimeout (node:timers:141:3)
at Object.<anonymous> (/Users/jefferson/Development/PROJECTS/blog-soaresdev/tempCodeRunnerFile.javascript:11:1)
at Module._compile (node:internal/modules/cjs/loader:1126:14)
at Object.Module._extensions..js (node:internal/modules/cjs/loader:1180:10)
at Module.load (node:internal/modules/cjs/loader:1004:32)
at Function.Module._load (node:internal/modules/cjs/loader:839:12)
at Function.executeUserEntryPoint [as runMain] (node:internal/modules/run_main:81:12)
at node:internal/main/run_main_module:17:47 {
code: 'ERR_INVALID_CALLBACK'
}
NOVAMENTE O MESMO ERRO DE undefined? ao observar com atenção notaremos que
cometemos um erro, ao passar a função secondPokemon()
como callback, abrimos
parêtenses na primeira função firstPokemon
fazendo com que essa função seja invocada
imediatamente e assim entao para de ser passada como callback.
Para corrigir isso, devemos utilizar um recurso chamado arrow functions
(funções anônimas)
na primeira função passada como callback, neste caso nossa função firstPokemon
.
Exemplo rápido sobre função anônima
function funcaoNaoAnonima() {
console.log('Este é um exemplo de uma função não anônima');
}
() => console.log('Este é um exemplo de função anônima');
Ao criar uma função anônima que tem como única utilidade retornar a invocação
de firstPokemon
, podemos manter secondPokemon
em sua forma de callback.
function firstPokemon(callback) {
console.log('Charmander');
setTimeout(callback, 2000);
}
function secondPokemon() {
console.log('Squirtle');
}
setTimeout(() => firstPokemon(secondPokemon), 3000);
//OUTPUT:
// ... 3 segundos
// Charmander
// .. 2 segundos
// Squirtle
Agora sim! Neste caso a função anônima será a chave para destrancarmos o poder
das callbacks. Para mergulhar um pouco mais afundo nessa maravilhosa funcionalidade,
iremos adicionar um último degrau de complexidade em nossos exemplos. Dessa vez
faremos com que secondPokemon
também receba um parâmetro. Neste caso será o pokemon
que gostaríamos de imprimir no console.
function firstPokemon(callback) {
console.log('Charmander');
setTimeout(callback, 2000);
}
function secondPokemon(pokemon) {
console.log(pokemon);
}
setTimeout(() => firstPokemon(() => secondPokemon('Squirtle')), 3000);
//OUTPUT:
// ... 3 segundos
// Charmander
// .. 2 segundos
// Squirtle
Problema do uso das Callbacks (Callback Hell)
Através das callbacks, somos capazes de manipular a ordem de execução das funções. Até aqui os exemplos foram simples e didáticos. No entanto, o mundo real é mais complicado e algumas aplicações se tornam complexas.
O uso das callbacks em projetos complexos cria um problema de legibilidade de código e torna seu uso mais difícil, quer ver? Vamos para um exemplo onde temos 5 funções que imprimem mensagens, uma sendo executada 2 segundos depois da outra e recebendo a mensagem como parâmetro. Como essas funções seriam escritas normalmente?
function primeiraMensagem(callback, mensagem) {
console.log(mensagem);
setTimeout(callback, 2000);
}
function segundaMensagem(callback, mensagem) {
console.log(mensagem);
setTimeout(callback, 2000);
}
function terceiraMensagem(callback, mensagem) {
console.log(mensagem);
setTimeout(callback, 2000);
}
function quartaMensagem(callback, mensagem) {
console.log(mensagem);
setTimeout(callback, 2000);
}
function quintaMensagem(mensagem) {
console.log(mensagem);
}
setTimeout(() => primeiraMensagem(
() => segundaMensagem(
() => terceiraMensagem(
() => quartaMensagem(
() => quintaMensagem('Quinta mensagem'),
'Quarta mensagem')
, 'Terceira mensagem')
, 'Segunda mensagem')
, 'Primeira mensagem'), 3000);
// OUTPUT:
// Primeira mensagem
// Segunda mensagem
// Terceira mensagem
// Quarta mensagem
// Quinta mensagem
Veja que a medida que são adicionadas novas chamadas de callbacks, a complexidade irá aumentar e a compreensão do código ficará mais difícil. Em casos mais extremos seu código vai parecer que foi cortado ao meio pelo Hadouken do Ryuu.
Felizmente existe uma maneira de evitar que esse tipo de problema aconteça. Estamos falando das Promises, aqui no blog tem post explicando sobre esse assunto também.