Come attendere la risoluzione di una promessa JavaScript prima di riprendere la funzione?


107

Sto facendo dei test unitari. Il framework di test carica una pagina in un iFrame e quindi esegue le asserzioni su quella pagina. Prima dell'inizio di ogni test, creo un messaggio Promiseche imposta l' onloadevento dell'iFrame da chiamare resolve(), imposta l'iFrame srce restituisce la promessa.

Quindi, posso semplicemente chiamare loadUrl(url).then(myFunc)e aspetterà il caricamento della pagina prima di eseguire qualunque cosa myFuncsia.

Uso questo tipo di pattern dappertutto nei miei test (non solo per caricare gli URL), principalmente per consentire modifiche al DOM (ad es. Mimare il clic su un pulsante e attendere che i div vengano nascosti e mostrati).

Lo svantaggio di questo design è che scrivo costantemente funzioni anonime con poche righe di codice. Inoltre, anche se ho un work-around (QUnit's assert.async()), la funzione di test che definisce le promesse viene completata prima che la promessa venga eseguita.

Mi chiedo se esiste un modo per ottenere un valore da a Promiseo attendere (blocco / sospensione) finché non si è risolto, in modo simile a .NET IAsyncResult.WaitHandle.WaitOne(). So che JavaScript è a thread singolo, ma spero che ciò non significhi che una funzione non possa produrre.

In sostanza, c'è un modo per ottenere quanto segue per sputare i risultati nell'ordine corretto?

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>


se metti le chiamate di accodamento in una funzione riutilizzabile, puoi quindi utilizzare () secondo necessità per DRY. puoi anche creare gestori multiuso, guidati da questo per alimentare le chiamate then (), come .then(fnAppend.bind(myDiv)), che possono ridurre notevolmente le anonime.
dandavis

Con cosa stai testando? Se è un browser moderno o puoi usare uno strumento come BabelJS per trasferire il tuo codice, questo è certamente possibile.
Benjamin Gruenbaum

Risposte:


78

Mi chiedo se esiste un modo per ottenere un valore da una promessa o attendere (blocco / sospensione) finché non si è risolto, in modo simile a IAsyncResult.WaitHandle.WaitOne () di .NET. So che JavaScript è a thread singolo, ma spero che ciò non significhi che una funzione non possa produrre.

L'attuale generazione di Javascript nei browser non ha un wait()o sleep()che consente l'esecuzione di altre cose. Quindi, semplicemente non puoi fare quello che chiedi. Invece, ha operazioni asincrone che faranno le loro cose e poi ti chiameranno quando hanno finito (come hai usato per le promesse).

In parte questo è dovuto all'unico threaded di Javascript. Se il thread singolo è in rotazione, nessun altro Javascript può essere eseguito fino a quando non viene eseguito il thread in rotazione. ES6 introduce yielde generatori che consentiranno alcuni trucchi cooperativi come questo, ma siamo abbastanza lontani dall'essere in grado di usarli in un ampio campione di browser installati (possono essere utilizzati in alcuni sviluppi lato server dove controlli il motore JS che viene utilizzato).


Una gestione attenta del codice basato sulle promesse può controllare l'ordine di esecuzione di molte operazioni asincrone.

Non sono sicuro di capire esattamente quale ordine stai cercando di ottenere nel tuo codice, ma potresti fare qualcosa del genere usando la tua kickOff()funzione esistente e quindi collegandovi un .then()gestore dopo averlo chiamato:

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);
});

Questo restituirà l'output in un ordine garantito, come questo:

start
middle
end

Aggiornamento nel 2018 (tre anni dopo che questa risposta è stata scritta):

Se si trasferisce il codice o lo si esegue in un ambiente che supporta le funzionalità ES7 come asynce await, è ora possibile utilizzarlo awaitper far "apparire" il codice in attesa del risultato di una promessa. Si sta ancora sviluppando con le promesse. Non blocca ancora tutto Javascript, ma ti consente di scrivere operazioni sequenziali in una sintassi più amichevole.

Invece del modo ES6 di fare le cose:

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

Puoi farlo:

// 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
});

3
Bene, usare then()è quello che ho fatto. È solo che non mi piace dover scrivere function() { ... }tutto il tempo. Ingombra il codice.
dx_over_dt

3
@dfoverdx - la codifica asincrona in Javascript implica sempre callback, quindi devi sempre definire una funzione (anonima o denominata). Al momento non c'è modo di aggirarlo.
jfriend00

2
Anche se puoi usare la notazione a freccia ES6 per renderlo più conciso. (foo) => { ... }invece difunction(foo) { ... }
Rob H,

1
Aggiungendo al commento @RobH abbastanza spesso puoi anche scrivere foo => single.expression(here)per eliminare le parentesi graffe e tonde.
Begie

3
@dfoverdx - Tre anni dopo la mia risposta, l'ho aggiornato con ES7 asynce la awaitsintassi come opzione che ora è disponibile in node.js e nei browser moderni o tramite transpilers.
jfriend00

15

Se usi ES2016 puoi usare asynce awaite fare qualcosa come:

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

Se usi ES2015 puoi usare Generatori . Se non ti piace la sintassi, puoi astrarla usando una asyncfunzione di utilità come spiegato qui .

Se usi ES5 probabilmente vorrai una libreria come Bluebird per darti più controllo.

Infine, se il tuo runtime supporta ES2015, l'ordine di esecuzione può essere mantenuto con il parallelismo utilizzando Fetch Injection .


1
Il tuo esempio di generatore non funziona, manca un corridore. Per favore non raccomandare più i generatori quando puoi semplicemente transpilare ES8 async/ await.
Bergi

3
Grazie per il tuo feedback. Ho rimosso l'esempio di Generator e collegato a un tutorial Generator più completo con la relativa libreria OSS.
Josh Habdas

9
Questo avvolge semplicemente la promessa. La funzione asincrona non aspetterà nulla quando la esegui, restituirà solo una promessa. async/ awaitè solo un'altra sintassi per Promise.then.
rustyx

Hai assolutamente ragione async, non aspetterò ... a meno che non ceda l' uso await. L'esempio ES2016 / ES7 non può ancora essere eseguito COSÌ, quindi ecco un po 'di codice che ho scritto tre anni fa per dimostrare il comportamento.
Josh Habdas

7

Un'altra opzione è utilizzare Promise.all per attendere la risoluzione di una serie di promesse. Il codice seguente mostra come attendere che tutte le promesse si risolvano e quindi gestire i risultati una volta che sono tutte pronte (poiché questo sembrava essere l'obiettivo della domanda); Tuttavia, start potrebbe ovviamente essere visualizzato prima che middle si sia risolto semplicemente aggiungendo quel codice prima che chiami la risoluzione.

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>


1

Puoi farlo manualmente. (Lo so, questa non è un'ottima soluzione, ma ..) usa il whileciclo finché resultnon ha un valore

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});

questo consumerebbe l'intera CPU durante l'attesa.
Lukasz Guminski

questa è una soluzione orribile
JSON
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.