Comment attendre la résolution d'une promesse JavaScript avant de reprendre la fonction?

107

Je fais des tests unitaires. Le framework de test charge une page dans un iFrame, puis exécute des assertions sur cette page. Avant le début de chaque test, je crée un Promisequi définit l' onloadévénement de l'iFrame à appeler resolve(), définit l'iFrame srcet renvoie la promesse.

Donc, je peux simplement appeler loadUrl(url).then(myFunc), et il attendra que la page se charge avant d'exécuter quoi que ce myFuncsoit.

J'utilise ce type de modèle partout dans mes tests (pas seulement pour charger des URL), principalement pour permettre aux modifications du DOM de se produire (par exemple, imiter en cliquant sur un bouton, et attendre que les divs se masquent et s'affichent).

L'inconvénient de cette conception est que j'écris constamment des fonctions anonymes contenant quelques lignes de code. De plus, même si j'ai une solution de contournement (QUnit assert.async()), la fonction de test qui définit les promesses se termine avant que la promesse ne soit exécutée.

Je me demande s'il existe un moyen d'obtenir une valeur de a Promiseou d'attendre (bloquer / dormir) jusqu'à ce qu'il soit résolu, similaire à .NET IAsyncResult.WaitHandle.WaitOne(). Je sais que JavaScript est monothread, mais j'espère que cela ne signifie pas qu'une fonction ne peut pas céder.

En gros, y a-t-il un moyen d'obtenir les résultats suivants pour recracher les résultats dans le bon ordre?

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
la source
si vous mettez les appels d'ajout dans une fonction réutilisable, vous pouvez alors () selon vos besoins pour DRY. vous pouvez également créer des gestionnaires polyvalents, guidés par cela pour alimenter les appels then (), comme .then(fnAppend.bind(myDiv)), ce qui peut réduire considérablement les anons.
dandavis
Avec quoi testez-vous? Si c'est un navigateur moderne ou si vous pouvez utiliser un outil comme BabelJS pour transpiler votre code, c'est certainement possible.
Benjamin Gruenbaum

Réponses:

78

Je me demande s'il existe un moyen d'obtenir une valeur à partir d'une promesse ou d'attendre (bloquer / dormir) jusqu'à ce qu'elle soit résolue, similaire à IAsyncResult.WaitHandle.WaitOne () .NET. Je sais que JavaScript est monothread, mais j'espère que cela ne signifie pas qu'une fonction ne peut pas céder.

La génération actuelle de Javascript dans les navigateurs n'a pas de wait()ou sleep()qui permet à d'autres choses de fonctionner. Donc, vous ne pouvez tout simplement pas faire ce que vous demandez. Au lieu de cela, il a des opérations asynchrones qui feront leur travail, puis vous appelleront quand elles auront terminé (comme vous avez utilisé les promesses pour).

Ceci est en partie dû au threading unique de Javascript. Si le thread unique tourne, aucun autre Javascript ne peut s'exécuter tant que ce thread ne tourne pas. ES6 introduit yieldet générateurs qui permettront certaines astuces coopératives comme celle-ci, mais nous sommes assez loin de pouvoir les utiliser dans un large éventail de navigateurs installés (ils peuvent être utilisés dans certains développements côté serveur où vous contrôlez le moteur JS qui est utilisé).


Une gestion attentive du code basé sur la promesse peut contrôler l'ordre d'exécution de nombreuses opérations asynchrones.

Je ne suis pas sûr de comprendre exactement l'ordre que vous essayez d'obtenir dans votre code, mais vous pouvez faire quelque chose comme ça en utilisant votre kickOff()fonction existante , puis en y attachant un .then()gestionnaire après l'avoir appelé:

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

Cela renverra la sortie dans un ordre garanti - comme ceci:

start
middle
end

Mise à jour en 2018 (trois ans après la rédaction de cette réponse):

Si vous transpilez votre code ou exécutez votre code dans un environnement qui prend en charge les fonctionnalités ES7 telles que asyncet await, vous pouvez maintenant utiliser awaitpour faire "apparaître" votre code pour attendre le résultat d'une promesse. Il se développe toujours avec des promesses. Il ne bloque toujours pas tout Javascript, mais il vous permet d'écrire des opérations séquentielles dans une syntaxe plus conviviale.

Au lieu de la façon de faire ES6:

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

Tu peux le faire:

// 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
la source
3
Bien, utiliser then()est ce que j'ai fait. Je n'aime pas avoir à écrire function() { ... }tout le temps. Cela encombre le code.
dx_over_dt
3
@dfoverdx - le codage asynchrone en Javascript implique toujours des callbacks, vous devez donc toujours définir une fonction (anonyme ou nommée). Pas moyen de le contourner actuellement.
jfriend00
2
Bien que vous puissiez utiliser la notation de flèche ES6 pour la rendre plus concise. (foo) => { ... }au lieu defunction(foo) { ... }
Rob H
1
En ajoutant souvent des commentaires @RobH, vous pouvez également écrire foo => single.expression(here)pour vous débarrasser des crochets bouclés et ronds.
Begie
3
@dfoverdx - Trois ans après ma réponse, je l'ai mise à jour avec ES7 asyncet la awaitsyntaxe en option qui est maintenant disponible dans node.js et dans les navigateurs modernes ou via des transpilers.
jfriend00
15

Si vous utilisez ES2016 vous pouvez utiliser asyncet awaitfaire quelque chose comme:

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

Si vous utilisez ES2015, vous pouvez utiliser des générateurs . Si vous n'aimez pas la syntaxe, vous pouvez la résumer en utilisant une asyncfonction utilitaire comme expliqué ici .

Si vous utilisez ES5, vous voudrez probablement une bibliothèque comme Bluebird pour vous donner plus de contrôle.

Enfin, si votre environnement d'exécution prend en charge ES2015, l'ordre d'exécution peut être conservé avec le parallélisme à l'aide de Fetch Injection .

Josh Habdas
la source
1
Votre exemple de générateur ne fonctionne pas, il manque un coureur. Veuillez ne plus recommander de générateurs lorsque vous pouvez simplement transpiler ES8 async/ await.
Bergi
3
Merci pour vos commentaires. J'ai supprimé l'exemple Generator et lié à un didacticiel Generator plus complet avec la bibliothèque OSS associée à la place.
Josh Habdas
9
Cela résume simplement la promesse. La fonction async ne pas attendre quoi que ce soit lorsque vous l' exécutez, il suffit de retourner une promesse. async/ awaitest juste une autre syntaxe pour Promise.then.
rustyx
Vous avez absolument raison de asyncne pas attendre ... à moins que cela ne cède à l' utilisation await. L'exemple ES2016 / ES7 ne peut toujours pas être exécuté SO alors voici un code que j'ai écrit il y a trois ans pour démontrer le comportement.
Josh Habdas
7

Une autre option consiste à utiliser Promise.all pour attendre la résolution d'un ensemble de promesses. Le code ci-dessous montre comment attendre que toutes les promesses se résolvent, puis traiter les résultats une fois qu'ils sont tous prêts (car cela semblait être l'objectif de la question); Cependant, start pourrait évidemment sortir avant que middle ait résolu simplement en ajoutant ce code avant d'appeler résoudre.

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
la source
1

Vous pouvez le faire manuellement. (Je sais que ce n'est pas une bonne solution, mais ..) utilisez la whileboucle jusqu'à ce que le resultn'ait pas de valeur

kickOff().then(function(result) {
   while(true){
       if (result === undefined) continue;
       else {
            $("#output").append(result);
            return;
       }
   }
});
Guga Nemsitsveridze
la source
cela consommerait la totalité du processeur en attendant.
Lukasz Guminski le
c'est une solution horrible
JSON