Maneira correta de aguardar uma função terminar antes de continuar?

187

Eu tenho duas funções JS. Um chama o outro. Dentro da função de chamada, eu gostaria de chamar a outra, aguardar a conclusão da função e continuar. Então, por exemplo / pseudo código:

function firstFunction(){
    for(i=0;i<x;i++){
        // do something
    }
};

function secondFunction(){
    firstFunction()
    // now wait for firstFunction to finish...
    // do something else
};

Eu vim com essa solução, mas não sei se essa é uma maneira inteligente de fazer isso.

var isPaused = false;

function firstFunction(){
    isPaused = true;
    for(i=0;i<x;i++){
        // do something
    }
    isPaused = false;
};

function secondFunction(){
    firstFunction()
    function waitForIt(){
        if (isPaused) {
            setTimeout(function(){waitForIt()},100);
        } else {
            // go do that thing
        };
    }
};

Isso é legítimo? Existe uma maneira mais elegante de lidar com isso? Talvez com jQuery?

DA.
fonte
11
O que firstFunctionexatamente isso faz com que seja assíncrono? De qualquer maneira - verificação sobre promessas
zerkms
A primeira função é atualizar rapidamente um relógio de pontuação a cada 10 segundos. O que ... vamos pensar nisso, suponho que poderíamos pré-calcular e depois pausar manualmente a segunda chamada de função via setTimeout. Dito isto, ainda vejo o desejo de ter uma capacidade de pausa em outro lugar.
DA.
você não precisa de uma pausa - google para promessas em jQuery
zerkms
O @zerkms promete parecer interessante! Ainda investigando o suporte ao navegador ...
DA.
1
Eu também tenho o mesmo problema.
Vappor Washmade

Respostas:

141

Uma maneira de lidar com o trabalho assíncrono como esse é usar uma função de retorno de chamada, por exemplo:

function firstFunction(_callback){
    // do some asynchronous work
    // and when the asynchronous stuff is complete
    _callback();    
}

function secondFunction(){
    // call first function and pass in a callback function which
    // first function runs when it has completed
    firstFunction(function() {
        console.log('huzzah, I\'m done!');
    });    
}

Conforme a sugestão de @Janaka Pushpakumara, agora você pode usar as funções de seta para obter a mesma coisa. Por exemplo:

firstFunction(() => console.log('huzzah, I\'m done!'))


Atualização: eu respondi isso há algum tempo e realmente quero atualizá-lo. Embora os retornos de chamada sejam absolutamente bons, na minha experiência, eles tendem a resultar em código mais difícil de ler e manter. Existem situações em que eu ainda as uso, como passar eventos em andamento e coisas do tipo como parâmetros. Esta atualização é apenas para enfatizar alternativas.

Além disso, a pergunta original não specificallty mencionar assíncrona, assim, no caso de alguém está confuso, se a sua função é síncrono, ele vai bloquear quando chamado. Por exemplo:

doSomething()
// the function below will wait until doSomething completes if it is synchronous
doSomethingElse()

Se, embora implícita, a função seja assíncrona, a maneira como costumo lidar com todo o meu trabalho assíncrono hoje é com async / waitit. Por exemplo:

const secondFunction = async () => {
  const result = await firstFunction()
  // do something else here after firstFunction completes
}

Na IMO, async / waitit torna seu código muito mais legível do que usar promessas diretamente (na maioria das vezes). Se você precisar lidar com erros de captura, use-o com try / catch. Leia mais sobre isso aqui: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function .

Matt Way
fonte
9
@zerkms - Gostaria de elaborar?
Matt Way
7
callbacks são inconvenientes para lidar com assincronia, promessas são muito mais fácil e flexível
zerkms
1
Enquanto você estiver certo de que eu não deveria ter usado a palavra melhor (atualizada), a conveniência de retornos de chamada versus promessas depende da complexidade do problema.
Matt Way
3
Torna-se offtopic, mas eu pessoalmente não vejo uma razão para preferir retornos de chamada hoje em dia :-) Não consigo lembrar de nenhum dos meus códigos escritos nos últimos dois anos desde que retornos de promessas.
Zerkms
3
Se desejar, você pode usar a função de seta em es6 secondFunction () {firstFunction ((response) => {console.log (response);}); }
Janaka Pushpakumara 18/08/19
53

Use assíncrono / espera:

async function firstFunction(){
  for(i=0;i<x;i++){
    // do something
  }
  return;
};

em seguida, use aguardar em sua outra função para aguardar o retorno:

async function secondFunction(){
  await firstFunction();
  // now wait for firstFunction to finish...
  // do something else
};
Fawaz
fonte
9
para aqueles de nós preso apoiar os navegadores mais antigos, o IE não suporta async / await
kyle
Isso pode ser usado fora de ambas as funções, como em $(function() { await firstFunction(); secondFunction(); });?
Lewistrick 21/01/19
1
@Lewistrick Lembre-se de awaitque só pode ser usado dentro de um asyncmétodo. Portanto, se você colocar seu pai em funcionamento, asyncpoderá ligar para o async methodsinterior, com ou sem o awaitque deseja.
Fawaz
É possível awaitum setTimeout?
Shayan
1
@ Shayan: Sim, envolva setTimeout em outra função que resolve uma promessa após o tempo limite.
Fawaz
51

Parece que você está perdendo um ponto importante aqui: JavaScript é um ambiente de execução de thread único. Vamos olhar novamente para o seu código, observe que eu adicionei alert("Here"):

var isPaused = false;

function firstFunction(){
    isPaused = true;
    for(i=0;i<x;i++){
        // do something
    }
    isPaused = false;
};

function secondFunction(){
    firstFunction()

    alert("Here");

    function waitForIt(){
        if (isPaused) {
            setTimeout(function(){waitForIt()},100);
        } else {
            // go do that thing
        };
    }
};

Você não precisa esperar isPaused. Quando você isPausedvir o alerta "Aqui", falsefirstFunctionestará e retornará. Isso ocorre porque você não pode "ceder" de dentro do forloop ( // do something), o loop pode não ser interrompido e terá que ser totalmente concluído primeiro (mais detalhes: manipulação de threads em Javascript e condições de corrida ).

Dito isso, você ainda pode fazer com que o código flua firstFunctionpara dentro para ser assíncrono e usar retorno de chamada ou promessa de notificar o chamador. Você teria que desistir do forloop e simular com ele if( JSFiddle ):

function firstFunction()
{
    var deferred = $.Deferred();

    var i = 0;
    var nextStep = function() {
        if (i<10) {
            // Do something
            printOutput("Step: " + i);
            i++;
            setTimeout(nextStep, 500); 
        }
        else {
            deferred.resolve(i);
        }
    }
    nextStep();
    return deferred.promise();
}

function secondFunction()
{
    var promise = firstFunction();
    promise.then(function(result) { 
        printOutput("Result: " + result);
    });
}

Em uma nota lateral, o JavaScript 1.7 introduziu a yieldpalavra-chave como parte dos geradores . Isso permitirá "perfurar" orifícios assíncronos no fluxo de código JavaScript síncrono ( mais detalhes e um exemplo ). No entanto, o suporte do navegador para geradores está atualmente limitado ao Firefox e Chrome, AFAIK.

noseratio
fonte
3
Você me salvou. $ .Deferred () é o que eu tenho procurado. Obrigado
Temitayo
@noseratio Eu tentei isso. Mas o método waitForIt não é chamado. O que estou fazendo de errado?
vigamage
@vigamage, você poderia fornecer um link para jsfiddle ou codepen do que você tentou?
noseratio 7/01
1
Olá, obrigado pela sua preocupação. Eu consegui funcionar. Obrigado..!
vigamage
18

Uma maneira elegante de esperar que uma função seja concluída primeiro é usar o Promises com a função assíncrona / aguardada .


  1. Em primeiro lugar, crie uma promessa . A função que eu criei será concluída após 2s. Usei setTimeoutpara demonstrar a situação em que as instruções levariam algum tempo para serem executadas.
  2. Para a segunda função, você pode usar a função assíncrona / aguardar , onde deseja que awaita primeira função seja concluída antes de prosseguir com as instruções.

Exemplo:

    //1. Create a new function that returns a promise
    function firstFunction() {
      return new Promise((resolve, reject) => {
          let y = 0
          setTimeout(() => {
            for(i=0; i<10; i++){
               y++
            }
             console.log('loop completed')  
             resolve(y)
          }, 2000)
      })
    }
    
    //2. Create an async function
    async function secondFunction() {
        console.log('before promise call')
        //3. Await for the first function to complete
        let result = await firstFunction()
        console.log('promise resolved: ' + result)
        console.log('next step')
    }; 

    secondFunction()


Nota:

Você poderia simplesmente resolveo Promisesem qualquer valor assim resolve(). No meu exemplo, eu resolvedo Promisecom o valor yque eu possa então usar na segunda função.

Jakub A Suplicki
fonte
sim, isso sempre funcionará. se alguém envolvido nesse tipo de cenário, o JavaScript Promise fará o truque por eles.
Puttamarigowda MS 01/07
5

O único problema com promessas é que o IE não as suporta. O Edge faz isso, mas existem muitos IE 10 e 11 por aí: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise (compatibilidade na parte inferior)

Portanto, o JavaScript é de thread único. Se você não estiver fazendo uma chamada assíncrona, ela se comportará de forma previsível. O thread principal do JavaScript executará uma função completamente antes de executar a próxima, na ordem em que aparecem no código. Garantir a ordem das funções síncronas é trivial - cada função será executada completamente na ordem em que foi chamada.

Pense na função síncrona como uma unidade atômica de trabalho . O principal encadeamento JavaScript o executará totalmente, na ordem em que as instruções aparecerem no código.

Mas, ative a chamada assíncrona, como na seguinte situação:

showLoadingDiv(); // function 1

makeAjaxCall(); // function 2 - contains async ajax call

hideLoadingDiv(); // function 3

Isso não faz o que você quer . Ele executa instantaneamente a função 1, a função 2 e a função 3. O carregamento de div pisca e desaparece, enquanto a chamada ajax não está quase completa, apesar de makeAjaxCall()ter retornado. A COMPLICAÇÃO é que makeAjaxCall()dividiu seu trabalho em partes que são avançadas pouco a pouco a cada rotação do encadeamento principal do JavaScript - está se comportando de maneira assícrona. Mas esse mesmo encadeamento principal, durante uma rodada / execução, executou as partes síncronas de forma rápida e previsível.

Então, do jeito que eu lidei com isso : como eu disse, a função é a unidade atômica do trabalho. Combinei o código da função 1 e 2 - coloquei o código da função 1 na função 2, antes da chamada assíncrona. Livrei-me da função 1. Tudo, inclusive a chamada assíncrona, é executada de maneira previsível, em ordem.

ENTÃO, quando a chamada assíncrona for concluída, após várias rotações do encadeamento JavaScript principal, faça com que ela chame a função 3. Isso garante a ordem . Por exemplo, com ajax, o manipulador de eventos onreadystatechange é chamado várias vezes. Quando ele relatar sua conclusão, chame a função final desejada.

Eu concordo que é mais confuso. Eu gosto de ter um código simétrico, de funções fazer uma coisa (ou quase), e não gosto de ter a chamada ajax de forma alguma responsável pela exibição (criando uma dependência no chamador). MAS, com uma chamada assíncrona incorporada em uma função síncrona, é necessário fazer compromissos para garantir a ordem de execução. E eu tenho que codificar para o IE 10, então não há promessas.

Resumo : para chamadas síncronas, garantir a ordem é trivial. Cada função é executada totalmente na ordem em que foi chamada. Para uma função com uma chamada assíncrona, a única maneira de garantir a ordem é monitorar quando a chamada assíncrona for concluída e chamar a terceira função quando esse estado for detectado.

Para uma discussão sobre threads JavaScript, consulte: https://medium.com/@francesco_rizzi/javascript-main-thread-dissected-43c85fce7e23 e https://developer.mozilla.org/en-US/docs/Web/JavaScript/ EventLoop

Além disso, outra pergunta semelhante e bem cotada sobre esse assunto: como devo chamar 3 funções para executá-las uma após a outra?

Sapo
fonte
8
Para o downvoter (s), eu ficaria curioso para saber o que há de errado com esta resposta.
21418 kermit
0

Foi assim que surgiu, pois preciso executar várias operações em uma cadeia.

<button onclick="tprom('Hello Niclas')">test promise</button>

<script>
    function tprom(mess) {
        console.clear();

        var promise = new Promise(function (resolve, reject) {
            setTimeout(function () {
                resolve(mess);
            }, 2000);
        });

        var promise2 = new Promise(async function (resolve, reject) {
            await promise;
            setTimeout(function () {
                resolve(mess + ' ' + mess);
            }, 2000);
        });

        var promise3 = new Promise(async function (resolve, reject) {
            await promise2;
            setTimeout(function () {
                resolve(mess + ' ' + mess+ ' ' + mess);
            }, 2000);
        });

        promise.then(function (data) {
            console.log(data);
        });

        promise2.then(function (data) {
            console.log(data);
        });

        promise3.then(function (data) {
            console.log(data);
        });
    }

</script>
Niclas Arnberger
fonte
Adicione pelo menos apenas comentários de alto nível para a descrição do snippet fornecido. Isso é para explicar como o seu código lida com o problema.
Cold Cerberus
O problema era executar uma segunda função após a primeira. Eu mostro como executar uma segunda função após a primeira e também como executar uma terceira função após a segunda. Eu criei três funções para facilitar a compreensão de qual código é necessário.
Niclas Arnberger 26/11/19
0

Sua diversão principal chamará firstFun e, depois disso, sua próxima diversão chamará.

async firstFunction() {
            const promise = new Promise((resolve, reject) => {
                for (let i = 0; i < 5; i++) {
                    // do something
                    console.log(i);
                    if (i == 4) {
                        resolve(i);
                    }
                }
            });
            const result = await promise;
        }

        second() {
            this.firstFunction().then( res => {
                // third function call do something
                console.log('Gajender here');
            });
        }
Gajender Singh
fonte