Como esperar que uma promessa de JavaScript seja resolvida antes de retomar a função?

107

Estou fazendo alguns testes de unidade. A estrutura de teste carrega uma página em um iframe e, em seguida, executa asserções nessa página. Antes de cada teste começar, eu crio um Promiseque define o onloadevento do iframe para chamar resolve(), define o do iframe srce retorna a promessa.

Então, eu posso apenas ligar loadUrl(url).then(myFunc)e ele vai esperar a página carregar antes de executar o que quer que myFuncseja.

Eu uso esse tipo de padrão em todos os lugares em meus testes (não apenas para carregar URLs), principalmente para permitir que mudanças no DOM aconteçam (por exemplo, imitar clicar em um botão e esperar que divs sejam ocultados e exibidos).

A desvantagem desse design é que estou constantemente escrevendo funções anônimas com algumas linhas de código. Além disso, enquanto eu tenho uma solução alternativa (QUnit'sassert.async() ), a função de teste que define as promessas é concluída antes que a promessa seja executada.

Estou me perguntando se existe alguma maneira de obter um valor de a Promiseou esperar (bloquear / suspender) até que seja resolvido, semelhante ao do .NET IAsyncResult.WaitHandle.WaitOne(). Eu sei que o JavaScript é de thread único, mas espero que isso não signifique que uma função não possa produzir.

Em essência, existe uma maneira de fazer com que o seguinte mostre os resultados na ordem correta?

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");
    
    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
};

function getResultFrom(promise) {
  // todo
  return " end";
}

var promise = kickOff();
var result = getResultFrom(promise);
$("#output").append(result);
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

dx_over_dt
fonte
se você colocar as chamadas de acréscimo em uma função reutilizável, você pode então () conforme necessário para DRY. você também pode fazer manipuladores multiuso, dirigidos por isso para alimentar chamadas then (), como .then(fnAppend.bind(myDiv)), que podem reduzir enormemente os anons.
dandavis
Com o que você está testando? Se for um navegador moderno ou você pode usar uma ferramenta como o BabelJS para transpilar seu código, isso certamente é possível.
Benjamin Gruenbaum

Respostas:

78

Estou me perguntando se há alguma maneira de obter um valor de uma promessa ou esperar (bloquear / suspender) até que seja resolvido, semelhante ao IAsyncResult.WaitHandle.WaitOne () do .NET. Eu sei que o JavaScript é de thread único, mas espero que isso não signifique que uma função não possa produzir.

A geração atual de Javascript nos navegadores não possui um wait()ou sleep()que permita que outras coisas sejam executadas. Então, você simplesmente não pode fazer o que está pedindo. Em vez disso, ele tem operações assíncronas que farão o seu trabalho e o chamarão quando terminar (como você vem usando promessas).

Parte disso é devido ao único threadedness do Javascript. Se o thread único estiver girando, nenhum outro Javascript poderá ser executado até que o thread de giro seja concluído. ES6 apresenta yielde geradores que permitirão alguns truques cooperativos como esse, mas estamos longe de poder usá-los em uma grande variedade de navegadores instalados (eles podem ser usados ​​em algum desenvolvimento do lado do servidor onde você controla o mecanismo JS que está sendo usado).


O gerenciamento cuidadoso do código baseado em promessa pode controlar a ordem de execução de muitas operações assíncronas.

Não tenho certeza se entendi exatamente a ordem que você está tentando alcançar em seu código, mas você poderia fazer algo assim usando sua kickOff()função existente e, em seguida, anexar um .then()manipulador a ela depois de chamá-la:

function kickOff() {
  return new Promise(function(resolve, reject) {
    $("#output").append("start");

    setTimeout(function() {
      resolve();
    }, 1000);
  }).then(function() {
    $("#output").append(" middle");
    return " end";
  });
}

kickOff().then(function(result) {
    // use the result here
    $("#output").append(result);
});

Isso retornará a saída em uma ordem garantida - como esta:

start
middle
end

Atualização em 2018 (três anos após a redação desta resposta):

Se você transpilar seu código ou executá-lo em um ambiente que ofereça suporte a recursos ES7 como asynce await, agora pode usar awaitpara fazer seu código "parecer" para aguardar o resultado de uma promessa. Ainda está se desenvolvendo com promessas. Ele ainda não bloqueia todo o Javascript, mas permite que você escreva operações sequenciais em uma sintaxe mais amigável.

Em vez da maneira ES6 de fazer as coisas:

someFunc().then(someFunc2).then(result => {
    // process result here
}).catch(err => {
    // process error here
});

Você consegue fazer isso:

// returns a promise
async function wrapperFunc() {
    try {
        let r1 = await someFunc();
        let r2 = await someFunc2(r1);
        // now process r2
        return someValue;     // this will be the resolved value of the returned promise
    } catch(e) {
        console.log(e);
        throw e;      // let caller know the promise was rejected with this reason
    }
}

wrapperFunc().then(result => {
    // got final result
}).catch(err => {
    // got error
});
jfriend00
fonte
3
Certo, usar then()é o que venho fazendo. Só não gosto de ter que escrever function() { ... }o tempo todo. Isso confunde o código.
dx_over_dt
3
@dfoverdx - a codificação assíncrona em Javascript sempre envolve callbacks, então você sempre tem que definir uma função (anônima ou nomeada). Nenhuma maneira de contornar isso atualmente.
jfriend00
2
Embora você possa usar a notação de seta ES6 para torná-la mais concisa. (foo) => { ... }em vez defunction(foo) { ... }
Rob H
1
Adicionando comentários do @RobH com frequência, você também pode escrever foo => single.expression(here)para se livrar dos colchetes.
início em
3
@dfoverdx - Três anos após minha resposta, eu atualizei com ES7 asynce awaitsintaxe como uma opção que agora está disponível em node.js e em navegadores modernos ou via transpilers.
jfriend00
15

Se estiver usando ES2016 você pode usar asynce awaite fazer algo como:

(async () => {
  const data = await fetch(url)
  myFunc(data)
}())

Se estiver usando ES2015, você pode usar Geradores . Se você não gosta da sintaxe, pode abstraí-la usando uma asyncfunção de utilidade, conforme explicado aqui .

Se estiver usando ES5, você provavelmente vai querer uma biblioteca como o Bluebird para ter mais controle.

Finalmente, se o seu tempo de execução suportar ES2015, a ordem de execução pode ser preservada com paralelismo usando Fetch Injection .

Josh Habdas
fonte
1
Seu exemplo de gerador não funciona, está faltando um corredor. Por favor, não recomende mais geradores quando você pode apenas transpilar ES8 async/ await.
Bergi
3
Obrigado pelo seu feedback! Removi o exemplo do Generator e vinculei-o a um tutorial mais abrangente do Generator com a biblioteca OSS relacionada.
Josh Habdas
9
Isso simplesmente encerra a promessa. A função assíncrona não espera por nada quando você a executa, apenas retorna uma promessa. async/ awaité apenas outra sintaxe para Promise.then.
rustyx
Você está absolutamente certo asyncnão vai esperar ... a menos que ceda usando await. O exemplo ES2016 / ES7 ainda não pode ser executado, então aqui está um código que escrevi três anos atrás, demonstrando o comportamento.
Josh Habdas
7

Outra opção é usar Promise.all para esperar que uma série de promessas sejam resolvidas. O código a seguir mostra como esperar que todas as promessas sejam resolvidas e, em seguida, lidar com os resultados quando estiverem todas prontas (pois esse parecia ser o objetivo da pergunta); No entanto, start pode obviamente sair antes que o meio seja resolvido, apenas adicionando esse código antes de chamar resolve.

function kickOff() {
  let start = new Promise((resolve, reject) => resolve("start"))
  let middle = new Promise((resolve, reject) => {
    setTimeout(() => resolve(" middle"), 1000)
  })
  let end = new Promise((resolve, reject) => resolve(" end"))

  Promise.all([start, middle, end]).then(results => {
    results.forEach(result => $("#output").append(result))
  })
}

kickOff()
<script src="https://ajax.googleapis.com/ajax/libs/jquery/2.1.1/jquery.min.js"></script>
<div id="output"></div>

Stan Kurdziel
fonte
1

Você pode fazer isso manualmente. (Eu sei, essa não é uma boa solução, mas ..) use whileloop até resultque não tenha um valor

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
fonte
isso consumiria toda a CPU durante a espera.
Lukasz Guminski
esta é uma solução horrível
JSON