Executando verificação de segurança...
5

[DÚVIDA] Ordem de callbacks

Salve pessoal!

Sendo bem sucinto, estava com essa dúvida sobre callbacks e fui pesquisando até encontrar no Stack uma pessoa com dúvida similar, porém não encontrei uma resposta que fosse clara (like a "ChatGPT me explique como se eu fosse um estudante de programação e não um dev sênior" kkkk)

Contexto

function f1(callback){
    setTimeout(function(){
        console.log('f1')
        if(callback)
            callback()
    }, 3000)
}

function f2(callback){
    setTimeout(function(){
        console.log('f2')
        if(callback)
            callback()
    }, 2000)
}

function f3(callback){
    setTimeout(function(){
        console.log('f3')
        if(callback)
            callback()
    }, 1000)
}

Bem, simples, temos 3 funções - f1, f2 e f3 - que possuem tempos de execução diferente - 3, 2 e 1 segundo respectivamente.

Até aí tudo bem

f1(function(){
    f2(function(){
        f3(function(){
            console.log('Terminei') //Meramente para mostrar que chegou até o F3
        })
    })
})

Quando executamos o código desse modo, ele executa na ordem pretendida de F1, F2 e F3.

Se executarmos, por exemplo:

f1() //F1

ou

f1(f2) //F1 -> F2

Até então funciona normalmente na ordem esperada, apresentando 1 e 2 respectivamente, independente do tempo.

O problema:

Até tentar expressar a execução da função de modo mais resumido (anônima), ele para de obedecer a ordem pretendida:

f1(f2(f3)) // F2 -> F3 -> F1 F****

Desse modo ele executa F2 -> F3 e depois F1 🤯🧠🔥 e se vc executar F3 dentro de F2:

f1(f2(f3())) // F3 -> F2 -> F1 - O moonwalker do callback

Como fazer ele respeitar a ordem de F1, F2 e F3?

Ainda estou em procura da resposta mas se algum jovem Jedi puder ajudar a esclarecer eu e a outros milhares de brasileiros que não dominam callbacks, agradeço muito.

E sem querer abusar, mas se vc responder, como e onde vc aprendeu sobre isso? Livro, vida, site?

Particularmente eu estou estudando nas documentações/sites mais técnicos mas confesso que por vezes fico perdido no tecniquês.

É isso pessoal, obrigado desde já!

Carregando publicação patrocinada...
4

Para respeitar a ordem de f1, f2 e f3, a primeira coisa que precisa fazer é mudar o tempo de espera para cada um executar.

Executa na ordem que você determinou que deve esperar para começar a executar. Isso nada tem a ver com callback. Sem colocar todos com o mesmo tempo fica impossível determinar ordem, porque o tempo é que determinará.

Quando quer testar uma coisa, teste só aquela coisa, nunca coloque outro componente junto porque aí não sabe qual está fazendo dar aquele resultado. Ou seja, se quer saber de callback, tire evento controlado por relógio, isso não faz sentido para o problema.

Se colocar tudo 1000ms ainda executará em ordem inversa, porque é assim que funciona a chamada. Isso nada tem a ver com callback, tem a ver com precedência de execução normal da linguagem. O primeiro a executar é sempre o que está dentro de parênteses. Portanto o f3() é o que está mais dentro é o primeiro a executar, depois o f2(), e finalmente o f1()que não está dentro da parênteses.

Portanto, se quer executar na ordem numérica, tem que inverter a chamada. Se quer que o f1() seja o primeiro a ser executado, ele tem que estar dentro dos parênteses mais internos.

f3(f2(f1()));

Também pode fazer:

f1();
f2();
f3();

Quando você faz:

f1(f2);

Está chamando só a f1() aí. Aqui começa entrar callback. Você não está chamando f2. Eu sei disso porque não tem os parênteses, que é a forma de indicar para o compilador que deseja a execução daquele identificar que eve ser uma função (o objeto nessa variável é uma function). Sem os parênteses é como passar um objeto de uma variável, não é uma chamada para executar. Então f1() recebe f2 no parâmetro. E ela será executada lá dentro.

Note que o parâmetro chama callback, então o objeto que estava em f2 agora está em callback pela passagem durante a chamada da função. Ela é executada depois de imprimir f1, por isso que você vê depois, se inverter a ordem e colocar o if e o bloco dele antes do console.log(), aparecerá f2 -> f1. Isso é fluxo de execução do mais básico.

Se você passar o objeto que é uma função, não tem como passar um parâmetro (tem técnicas se for necessário, mas não da mesma forma, e é mais avançado, e quase sempre é um erro). Então teve que fazer duas chamadas de função, por precedência, que é a mesma coisa que fazer:

f2(f3);
f1();

Ou seja, chamou f2() passando o objeto de f3 para executar lá dentro, e depois chamou f1().

E como em f1() não está passando nada e antes de executar o código testa se há um objeto válido, e neste caso não tem, ele não chama nada.

Já em:

f1(function(){
    f2(function(){
        f3(function(){
            console.log('Terminei') //Meramente para mostrar que chegou até o F3
        })
    })
})

Todos os parêmtros são callbacks. Está bem explícito. O código está chamando f1() passando como argumeto um callback que chama f2(), que por sua vez passa como argumeto um callback que chama f3(), então dentro de f1(), ele chama f2(), que por sua vez chama f3(), na ordem que você queria.

A diferença é que você está criando uma função anônima na hora para depois dar callback, e um extra. Quando só usa o identificador, não deixa de ser uma função, não anônima, e que depois pode ser dado um callback, porque em JS para chamar uma função não importa com oa função foi declarada.

Tudo é que estão de onde está sendo executado. Em resumo, com parêntese chama, sem ele não chama, é só um objeto. Só lembrando que function() é uma declaração e não uma chamada, é uma palavra-chave com significado especial.

Se você mandar executar com um debugger ligado, mandando executar linha por linha ajuda entender o funcionamento.

Uma das coisas que faz as pessoas se darem mal no Stack Overflow é que elas não colocam o MCVE, que é uma das coisas mais básicas da programação. Então não é o iniciante que se dá mal lá, é quem é iniciante sem agir como iniciante. Por que estou falando disso? Porque seu exemplo não faz o MCVE. Ele coloca ruído, coloca coisas que nada tem a ver com o problema. E aí fica difícil de entender.

Em alguns casos porque está pulando etapas. O que torna difícil entender algumas coisas. Porque está agindo como se fosse um programador que já abe coisas mais avançadas (que muitas pessoas vão dizer que ele é sênior, mas isso nada tem a ver com senioridade). Inclusive o sênior aprende que não pode pular etapas.

Aprender debugar é uma das coisas mais básicas e importantes que um estudante de programação deve fazer, até para aprender a entedner oque está fazendo. Não é uma técnica avançada ("de sênior"). E muita gente hoje faz códigos complexos (em geral com ^C^V) e não sabem depurar um código.

Daí começo responendo que aprendi por livros, com a vida, talvez tenha ajudado em algum momento em algum site, porque quando eu aprendi não existiam sites, mas cosias mais avançadas foram alguns. Eu aprendo de forma estruturada, em uma ordem que faça sentido. Demora. Parece chato, você não vê resultados bonitinhos na hora, até desanima quem precisa demais de estímulos. Mas é a forma correta para aprender corretamente. Eu sei que muita gente não quer, ou até não consegue segurar o ímpeto de ir logo para o que estimula mais, mas prejudica todo futuro.

Muitas vezes falta a pessoa seguir os links fornecidos. Geralmente os links que eu coloco são clicados por menos de 20% das pessoas, alguns casos bem menos. Claramente a pessoa está fugindo do aprendizado (não deveria ser perto de 100% porque muitas pessoas não precisam clicar mesmo).

Se está perdido com os termos é porque os pulou. Então fica difícil explicar corretamente. Alguns explicam e fica torto, porque para simplificar acaba ensinando errado. Tem que voltar atrás. Tem que entender precedência e como o código será executado, antes de tentar aprender callback, ou eventos. Tem que aprender que apesar da linguagem aceitar não usar ;, tem caso que dá problemas, e que ela não foi bem pensada para funcionar assim, então é melhor usar sempre, mesmo que algumas pessoas ensinam assim para fazer graça. E são as mesmas pessoas que pregam legibilidade que fazem assim, mostrando que sequer entendo o que é legível, o que te faz ficar pensando sobre o que deveria ser irrelevante, ou ter erros "estranhos" quando pega a exceção.

Não vai entedner essas coisas se não souber oque é variável, objeto, função, como funcionam, parâmetros, argumentos, precedência, controle de fluxo, outros elementos do código, fazer teste de mesa, callback, avançado, valor e referência, etc. Só passei algumas coisas, tem muito mais material, lá e em outros lugares como a Wikipedia, C2 wiki, o SOen, etc. Sem entrar em asssuntos específicos foram os 3 que mais me ajudaram. Além de documentação para aprender tecnologia (não aprender a programar).

Parece bobo ou avançado, mas ajuda entender. Também.

Eu sou visto com o chato que fala em estudar os fundamentos, não pular etapas, de aprender programar corretamente, que programação é matemática, lógica, mas justamente porque as pessoas ignoram que está tão difícil preencher as vagas. Eu não sei mais o que fazer para fazer as pessoas apreciarem o que importa. Quem estuda corretamente não teme ChatGPT ou diminuição do número de vagas. Espero que isso ajude mais que só dizer porque funciona ou não, que nem sei se consegui fazer entender.

Aprendizado é um processo, é uma maratona. Ou várias. É uma vida. Quem tenta correr como se fosse 100mts rasos chama-se coelho, e para no meio do caminho, ou chega muito lá atrás. Dá trabalho? É cansativo? Sim. Talvez dancinha no TikTok seja mais fácil.

Se preocupar com esse ponto já é acima da média, muita gente só quer ver o resultado. Por iss oeu gastei meu tempo respondendo, acho que posso ajudar o OP e outros, se não explicando bem, pelo menos tentando colocar no caminho certo.

Faz sentido para você?

Espero ter ajudado.

Farei algo que muitos pedem para aprender a programar corretamente, gratuitamente. Para saber quando, me segue nas suas plataformas preferidas. Quase não as uso, não terá infindas notificações (links aqui).

2

Só pra complementar o que já foi dito, o setTimeout só está servindo para confundir. Isso porque cada callback vai executar depois de determinado tempo, e como os tempos são diferentes, isso acaba confundindo, pois você não sabe se algo executou depois porque realmente foi esta a ordem, ou é só porque o tempo era maior.

Então para simplificar, e deixar mais claro o que está acontecendo, mudei as suas funções, retirando o setTimeout e deixando-as assim:

function f1(callback) {
    console.log('f1');
    if (callback)
        callback();
    else
        console.log('f1 sem callback');
}

function f2(callback) {
    console.log('f2');
    if (callback)
        callback();
    else
        console.log('f2 sem callback');
}

function f3(callback) {
    console.log('f3');
    if (callback)
        callback();
    else
        console.log('f3 sem callback');
}

Agora, ao rodar o primeiro exemplo:

f1(function() {
    f2(function() {
        f3(function() {
            console.log('Terminei');
        })
    })
});

A saída é:

f1
f2
f3
Terminei

Pois primeiro ele chama f1, passando uma função anônima (function () { etc). Esta função anônima é o callback passado para f1. E como f1 executa o callback, então ela será executada. E dentro dela é chamada f2, que por sua vez recebe outro callback, que por fim chama f3.

No fundo, este código é equivalente a:

function callback1() {
    f2(callback2)
}
function callback2() {
    f3(callback3);
}
function callback3() {
    console.log('Terminei');
}

f1(callback1);

A diferença é que o primeiro código, em vez de ter as funções callbackX, usou funções anônimas, e foi tudo feito de uma vez.

Olhando o código acima, agora dá pra entender porque executa na ordem f1, f2, f3:

  • f1(callback1) é executada
  • dentro de f1, chama o console.log('f1'); e em seguida executa callback1
  • mas callback1 chama f2(callback2)
  • dentro de f2, chama o console.log('f2'); e em seguida executa callback2
  • mas callback2 chama f3(callback3)
  • dentro de f3, chama o console.log('f3'); e em seguida executa callback3
  • callback3 chama console.log('Terminei');

Prosseguindo com seus exemplos, se executarmos f1(), a saída é:

f1
f1 sem callback

Repare que agora chamamos f1() sem passar nenhum argumento, portanto o parâmetro callback não será setado e entrará no else.

E se chamarmos f1(f2), a saída é:

f1
f2
f2 sem callback

Pois agora estamos chamando f1 e passando f2 como argumento. Repare que f2, sem os parênteses, indica que estou passando a função como argumento (em vez de executá-la). Neste caso, f2 é o callback. Então dentro de f1 ele verifica se o callback está setado, e como está, então executa. Por isso f2 é executada, mas como ela é chamada sem argumentos, então f2 não recebe um callback acaba entrando no else.


E se fizermos f1(f2(f3)), a saída será:

f2
f3
f3 sem callback
f1
f1 sem callback

Agora é o seguinte:

  • f1 é chamada com um argumento, que é f2(f3)
  • mas f2(f3) é outra chamada: f2 está sendo executada, e recebe f3 como argumento (ou seja, f3 é o callback de f2, já f3 não tem parênteses - não está sendo chamada, e sim passada como argumento para f2)
  • só que f2 está sendo chamada, então primeiro ela precisa terminar sua execução, para que o resultado seja passado para f1
  • por isso f2 é executada primeiro. E como seu callback é f3, esta é executada em seguida. Mas como f3 é chamada sem argumentos, entra no else
  • depois que f2 terminou, seu retorno é passado para f1. Mas f2 não retorna nada (não tem nenhum return), então nesses casos o JS definiu que o retorno é undefined
  • ou seja, f1 recebe undefined como callback, e como undefined é um valor considerado falso, entra no else

Por isso que f1 acaba sendo executada por último. Porque a chamada f2(f3) precisa terminar, para que seu retorno seja passado para f1.


Agora sobre f1(f2(f3())). O resultado é:

f3
f3 sem callback
f2
f2 sem callback
f1
f1 sem callback

Repare que f3() (com parênteses) está chamando f3, ou seja f2(f3()) está dizendo que f2 recebe como argumento o resultado da execução de f3. Por isso f3 tem que executar primeiro, pois só depois que ela terminar que o valor retornado pode ser passado para f2. E da mesma forma, o resultado de f2 é passado para f1, por isso que f2 tem que executar antes de f1.


Se quer respeitar a ordem, depende muito do que precisa. Uma função precisa do resultado da outra? Elas podem ser chamadas de maneira independente? Etc.

No seu exemplo não fica claro qual deve ser a ordem correta, até porque as funções não fazem nada de especial.

De qualquer forma, a ideia de receber um callback é: "me passe a função que será executada depois". O callback é isso, vc indica o que vai ser executado, mas o quando é decidido pela função que o recebeu (algumas podem ser imediatamente, outras só quando determinada condição ocorrer - por exemplo, no setTimeout, só executa depois que o tempo indicado se passou).