Comment renvoyer la réponse d'un appel asynchrone?

5512

J'ai une fonction fooqui fait une requête Ajax. Comment puis-je retourner la réponse foo?

J'ai essayé de renvoyer la valeur du successrappel, ainsi que d'attribuer la réponse à une variable locale à l'intérieur de la fonction et de renvoyer celle-ci, mais aucune de ces façons ne renvoie réellement la réponse.

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            result = response;
            // return response; // <- I tried that one as well
        }
    });

    return result;
}

var result = foo(); // It always ends up being `undefined`.
Felix Kling
la source

Réponses:

5704

→ Pour une explication plus générale du comportement asynchrone avec différents exemples, veuillez consulter Pourquoi ma variable n'est-elle pas modifiée après l'avoir modifiée à l'intérieur d'une fonction? - Référence de code asynchrone

→ Si vous comprenez déjà le problème, passez aux solutions possibles ci-dessous.

Le problème

Le A à Ajax signifie asynchrone . Cela signifie que l'envoi de la demande (ou plutôt la réception de la réponse) est retiré du flux d'exécution normal. Dans votre exemple, $.ajaxrenvoie immédiatement et l'instruction suivante,, return result;est exécutée avant même que la fonction que vous avez passée en tant que successrappel ne soit appelée.

Voici une analogie qui, nous l'espérons, fait la différence entre le flux synchrone et asynchrone plus clair:

Synchrone

Imaginez que vous appeliez un ami et lui demandiez de rechercher quelque chose pour vous. Bien que cela puisse prendre un certain temps, vous attendez au téléphone et regardez dans l'espace, jusqu'à ce que votre ami vous donne la réponse dont vous aviez besoin.

La même chose se produit lorsque vous effectuez un appel de fonction contenant du code "normal":

function findItem() {
    var item;
    while(item_not_found) {
        // search
    }
    return item;
}

var item = findItem();

// Do something with item
doSomethingElse();

Même si l' findItemexécution peut prendre beaucoup de temps, tout code suivant var item = findItem();doit attendre que la fonction renvoie le résultat.

Asynchrone

Vous appelez à nouveau votre ami pour la même raison. Mais cette fois, vous lui dites que vous êtes pressé et qu'il devrait vous rappeler sur votre téléphone portable. Vous raccrochez, quittez la maison et faites tout ce que vous avez prévu de faire. Une fois que votre ami vous rappelle, vous traitez avec les informations qu'il vous a données.

C'est exactement ce qui se passe lorsque vous faites une demande Ajax.

findItem(function(item) {
    // Do something with item
});
doSomethingElse();

Au lieu d'attendre la réponse, l'exécution se poursuit immédiatement et l'instruction après l'exécution de l'appel Ajax. Pour obtenir la réponse à terme, vous fournissez une fonction à appeler une fois la réponse reçue, un rappel (remarquez quelque chose? Rappelez-vous ?). Toute instruction venant après cet appel est exécutée avant l'appel du rappel.


Solutions)

Adoptez la nature asynchrone de JavaScript! Bien que certaines opérations asynchrones fournissent des homologues synchrones (tout comme "Ajax"), il est généralement déconseillé de les utiliser, en particulier dans un contexte de navigateur.

Pourquoi est-ce mauvais demandez-vous?

JavaScript s'exécute dans le thread d'interface utilisateur du navigateur et tout processus de longue durée verrouillera l'interface utilisateur, ce qui la rendra insensible. De plus, il y a une limite supérieure sur le temps d'exécution pour JavaScript et le navigateur demandera à l'utilisateur de poursuivre ou non l'exécution.

Tout cela est une très mauvaise expérience utilisateur. L'utilisateur ne pourra pas dire si tout fonctionne bien ou non. De plus, l'effet sera pire pour les utilisateurs avec une connexion lente.

Dans ce qui suit, nous examinerons trois solutions différentes qui se construisent toutes les unes sur les autres:

  • Promesses avecasync/await (ES2017 +, disponible dans les anciens navigateurs si vous utilisez un transpilateur ou un régénérateur)
  • Rappels (populaires dans le nœud)
  • Promesses avecthen() (ES2015 +, disponible dans les anciens navigateurs si vous utilisez l'une des nombreuses bibliothèques de promesses)

Les trois sont disponibles dans les navigateurs actuels et le nœud 7+.


ES2017 +: promesses avec async/await

La version ECMAScript publiée en 2017 a introduit la prise en charge au niveau syntaxique des fonctions asynchrones. Avec l'aide de asyncet await, vous pouvez écrire asynchrone dans un "style synchrone". Le code est toujours asynchrone, mais il est plus facile à lire / à comprendre.

async/awaits'appuie sur des promesses: une asyncfonction renvoie toujours une promesse. await"déballe" une promesse et aboutit à la valeur avec laquelle la promesse a été résolue ou génère une erreur si la promesse a été rejetée.

Important: Vous ne pouvez utiliser qu'à l' awaitintérieur d'une asyncfonction. Pour le moment, le niveau supérieur awaitn'est pas encore pris en charge, vous devrez donc peut-être créer une async IIFE ( Immediateely Invoked Function Expression ) pour démarrer un asynccontexte.

Vous pouvez en savoir plus sur asyncet awaitsur MDN.

Voici un exemple qui s'appuie sur le délai ci-dessus:

// Using 'superagent' which will return a promise.
var superagent = require('superagent')

// This is isn't declared as `async` because it already returns a promise
function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}


async function getAllBooks() {
  try {
    // GET a list of book IDs of the current user
    var bookIDs = await superagent.get('/user/books');
    // wait for 3 seconds (just for the sake of this example)
    await delay();
    // GET information about each book
    return await superagent.get('/books/ids='+JSON.stringify(bookIDs));
  } catch(error) {
    // If any of the awaited promises was rejected, this catch block
    // would catch the rejection reason
    return null;
  }
}

// Start an IIFE to use `await` at the top level
(async function(){
  let books = await getAllBooks();
  console.log(books);
})();

Prise en charge des versions actuelles du navigateur et des nœudsasync/await . Vous pouvez également prendre en charge des environnements plus anciens en transformant votre code en ES5 à l'aide de régénérateur (ou d'outils utilisant régénérateur, tels que Babel ).


Laisser les fonctions accepter les rappels

Un rappel est simplement une fonction passée à une autre fonction. Cette autre fonction peut appeler la fonction passée chaque fois qu'elle est prête. Dans le contexte d'un processus asynchrone, le rappel sera appelé chaque fois que le processus asynchrone est terminé. Habituellement, le résultat est transmis au rappel.

Dans l'exemple de la question, vous pouvez faire foo accepter un rappel et l'utiliser comme successrappel. Donc ça

var result = foo();
// Code that depends on 'result'

devient

foo(function(result) {
    // Code that depends on 'result'
});

Ici, nous avons défini la fonction "en ligne" mais vous pouvez passer n'importe quelle référence de fonction:

function myCallback(result) {
    // Code that depends on 'result'
}

foo(myCallback);

foo lui-même est défini comme suit:

function foo(callback) {
    $.ajax({
        // ...
        success: callback
    });
}

callbackfera référence à la fonction à foolaquelle nous passons lorsque nous l'appelons et nous la transmettons simplement à success. C'est-à-dire une fois que la demande Ajax est réussie,$.ajax appellera callbacket passera la réponse au rappel (auquel on peut se référer result, puisque c'est ainsi que nous avons défini le rappel).

Vous pouvez également traiter la réponse avant de la transmettre au rappel:

function foo(callback) {
    $.ajax({
        // ...
        success: function(response) {
            // For example, filter the response
            callback(filtered_response);
        }
    });
}

Il est plus facile d'écrire du code à l'aide de rappels qu'il n'y paraît. Après tout, JavaScript dans le navigateur est fortement piloté par les événements (événements DOM). La réception de la réponse Ajax n'est rien d'autre qu'un événement.
Des difficultés peuvent survenir lorsque vous devez travailler avec du code tiers, mais la plupart des problèmes peuvent être résolus en réfléchissant simplement au flux d'application.


ES2015 +: promesses avec then ()

L' API Promise est une nouvelle fonctionnalité d'ECMAScript 6 (ES2015), mais elle prend déjà en charge un bon navigateur . Il existe également de nombreuses bibliothèques qui implémentent l'API Promises standard et fournissent des méthodes supplémentaires pour faciliter l'utilisation et la composition des fonctions asynchrones (par exemple bluebird ).

Les promesses sont des conteneurs pour les valeurs futures . Lorsque la promesse reçoit la valeur (elle est résolue ) ou lorsqu'elle est annulée ( rejetée ), elle avertit tous ses "écouteurs" qui souhaitent accéder à cette valeur.

L'avantage par rapport aux rappels simples est qu'ils vous permettent de découpler votre code et qu'ils sont plus faciles à composer.

Voici un exemple simple d'utilisation d'une promesse:

function delay() {
  // `delay` returns a promise
  return new Promise(function(resolve, reject) {
    // Only `delay` is able to resolve or reject the promise
    setTimeout(function() {
      resolve(42); // After 3 seconds, resolve the promise with value 42
    }, 3000);
  });
}

delay()
  .then(function(v) { // `delay` returns a promise
    console.log(v); // Log the value once it is resolved
  })
  .catch(function(v) {
    // Or do something else if it is rejected 
    // (it would not happen in this example, since `reject` is not called).
  });

Appliqué à notre appel Ajax, nous pourrions utiliser des promesses comme celle-ci:

function ajax(url) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open('GET', url);
    xhr.send();
  });
}

ajax("/echo/json")
  .then(function(result) {
    // Code depending on result
  })
  .catch(function() {
    // An error occurred
  });

Décrire tous les avantages de cette promesse dépasse le cadre de cette réponse, mais si vous écrivez du nouveau code, vous devez sérieusement les considérer. Ils fournissent une excellente abstraction et séparation de votre code.

Plus d'informations sur les promesses: HTML5 rocks - JavaScript Promises

Note latérale: les objets différés de jQuery

Les objets différés sont l'implémentation personnalisée de promesses par jQuery (avant la normalisation de l'API Promise). Ils se comportent presque comme des promesses mais exposent une API légèrement différente.

Chaque méthode Ajax de jQuery retourne déjà un "objet différé" (en fait une promesse d'un objet différé) que vous pouvez simplement renvoyer de votre fonction:

function ajax() {
    return $.ajax(...);
}

ajax().done(function(result) {
    // Code depending on result
}).fail(function() {
    // An error occurred
});

Note de côté: promesses gotchas

Gardez à l'esprit que les promesses et les objets différés ne sont que des conteneurs pour une valeur future, ils ne sont pas la valeur elle-même. Par exemple, supposons que vous disposiez des éléments suivants:

function checkPassword() {
    return $.ajax({
        url: '/password',
        data: {
            username: $('#username').val(),
            password: $('#password').val()
        },
        type: 'POST',
        dataType: 'json'
    });
}

if (checkPassword()) {
    // Tell the user they're logged in
}

Ce code comprend mal les problèmes d'asynchronie ci-dessus. Plus précisément, $.ajax()ne gèle pas le code pendant qu'il vérifie la page `` / mot de passe '' sur votre serveur - il envoie une demande au serveur et pendant qu'il attend, il renvoie immédiatement un objet jQuery Ajax Deferred, pas la réponse du serveur. Cela signifie que l' ifinstruction va toujours obtenir cet objet différé, le traiter comme trueet procéder comme si l'utilisateur était connecté. Pas bon.

Mais la solution est simple:

checkPassword()
.done(function(r) {
    if (r) {
        // Tell the user they're logged in
    } else {
        // Tell the user their password was bad
    }
})
.fail(function(x) {
    // Tell the user something bad happened
});

Déconseillé: appels "Ajax" synchrones

Comme je l'ai mentionné, certaines (!) Opérations asynchrones ont des homologues synchrones. Je ne préconise pas leur utilisation, mais pour des raisons d'exhaustivité, voici comment effectuer un appel synchrone:

Sans jQuery

Si vous utilisez directement un XMLHTTPRequestobjet, passez falsecomme troisième argument à .open.

jQuery

Si vous utilisez jQuery , vous pouvez définir l' asyncoption sur false. Notez que cette option est déconseillée depuis jQuery 1.8. Vous pouvez alors soit toujours utiliser un successrappel, soit accéder à la responseTextpropriété de l' objet jqXHR :

function foo() {
    var jqXHR = $.ajax({
        //...
        async: false
    });
    return jqXHR.responseText;
}

Si vous utilisez une autre méthode jQuery Ajax, tels que $.get, $.getJSON, etc., vous devez changer à $.ajax(puisque vous ne pouvez passer des paramètres de configuration$.ajax ).

La tête haute! Il n'est pas possible de faire une demande JSONP synchrone . JSONP par sa nature même est toujours asynchrone (une raison de plus pour ne même pas considérer cette option).

Felix Kling
la source
74
@Pommy: Si vous souhaitez utiliser jQuery, vous devez l'inclure. Veuillez vous référer à docs.jquery.com/Tutorials:Getting_Started_with_jQuery .
Felix Kling
11
Dans la solution 1, sous jQuery, je ne pouvais pas comprendre cette ligne: If you use any other jQuery AJAX method, such as $.get, $.getJSON, etc., you have them to $.ajax.(Oui, je réalise que mon pseudo est un peu ironique dans ce cas)
cssyphus
32
@gibberish: Mmmh, je ne sais pas comment cela peut être rendu plus clair. Voyez-vous comment fooest appelé et qu'une fonction lui est passée ( foo(function(result) {....});)? resultest utilisé à l'intérieur de cette fonction et est la réponse de la requête Ajax. Pour faire référence à cette fonction, le premier paramètre de foo est appelé callbacket assigné à la successplace d'une fonction anonyme. Donc, $.ajaxj'appellera callbackquand la demande a réussi. J'ai essayé de l'expliquer un peu plus.
Felix Kling
43
Le chat pour cette question est mort, donc je ne sais pas où proposer les modifications décrites, mais je propose: 1) Modifiez la partie synchrone en une simple discussion sur les raisons pour lesquelles elle est mauvaise sans exemple de code sur la façon de le faire. 2) Supprimez / fusionnez les exemples de rappel pour ne montrer que l'approche différée plus flexible, qui, je pense, peut également être un peu plus facile à suivre pour ceux qui apprennent Javascript.
Chris Moschini
14
@ Jessi: Je pense que vous avez mal compris cette partie de la réponse. Vous ne pouvez pas utiliser $.getJSONsi vous souhaitez que la demande Ajax soit synchrone. Cependant, vous ne devez pas souhaiter que la demande soit synchrone, donc cela ne s'applique pas. Vous devez utiliser des rappels ou des promesses pour gérer la réponse, comme cela est expliqué plus haut dans la réponse.
Felix Kling du
1071

Si vous n'utilisez pas jQuery dans votre code, cette réponse est pour vous

Votre code devrait être quelque chose dans le sens de ceci:

function foo() {
    var httpRequest = new XMLHttpRequest();
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
    return httpRequest.responseText;
}

var result = foo(); // always ends up being 'undefined'

Felix Kling a fait un excellent travail en écrivant une réponse pour les personnes utilisant jQuery pour AJAX, j'ai décidé de fournir une alternative pour les personnes qui ne le sont pas.

( Remarque, pour ceux qui utilisent la nouvelle fetchAPI, Angular ou promesses, j'ai ajouté une autre réponse ci-dessous )


Ce à quoi vous faites face

Ceci est un bref résumé de "Explication du problème" de l'autre réponse, si vous n'êtes pas sûr après avoir lu ceci, lisez cela.

Le A dans AJAX signifie asynchrone . Cela signifie que l'envoi de la demande (ou plutôt la réception de la réponse) est retiré du flux d'exécution normal. Dans votre exemple, .sendrenvoie immédiatement et l'instruction suivante,, return result;est exécutée avant même que la fonction que vous avez passée en tant que successrappel ne soit appelée.

Cela signifie que lorsque vous revenez, l'écouteur que vous avez défini ne s'est pas encore exécuté, ce qui signifie que la valeur que vous retournez n'a pas été définie.

Voici une analogie simple

function getFive(){ 
    var a;
    setTimeout(function(){
         a=5;
    },10);
    return a;
}

(Violon)

La valeur de aretournée est undefinedpuisque la a=5partie n'a pas encore été exécutée. AJAX agit comme ceci, vous retournez la valeur avant que le serveur n'ait la possibilité de dire à votre navigateur quelle est cette valeur.

Une solution possible à ce problème consiste à coder de manière réactive , en indiquant à votre programme quoi faire une fois le calcul terminé.

function onComplete(a){ // When the code completes, do this
    alert(a);
}

function getFive(whenDone){ 
    var a;
    setTimeout(function(){
         a=5;
         whenDone(a);
    },10);
}

C'est ce qu'on appelle CPS . En gros, on passegetFive une action à effectuer lorsqu'elle se termine, nous disons à notre code comment réagir à la fin d'un événement (comme notre appel AJAX, ou dans ce cas le délai d'expiration).

L'utilisation serait:

getFive(onComplete);

Ce qui devrait alerter "5" à l'écran. (Violon) .

Solutions possibles

Il existe essentiellement deux façons de résoudre ce problème:

  1. Rendre l'appel AJAX synchrone (appelons-le SJAX).
  2. Restructurez votre code pour qu'il fonctionne correctement avec les rappels.

1. AJAX synchrone - Ne le faites pas !!

Quant à l'AJAX synchrone, ne le faites pas! La réponse de Felix soulève des arguments convaincants pour expliquer pourquoi c'est une mauvaise idée. Pour résumer, cela gèle le navigateur de l'utilisateur jusqu'à ce que le serveur renvoie la réponse et crée une très mauvaise expérience utilisateur. Voici un autre court résumé tiré de MDN sur pourquoi:

XMLHttpRequest prend en charge les communications synchrones et asynchrones. En général, cependant, les demandes asynchrones doivent être préférées aux demandes synchrones pour des raisons de performances.

En bref, les requêtes synchrones bloquent l'exécution du code ... ... cela peut provoquer de graves problèmes ...

Si vous devez le faire, vous pouvez passer un drapeau: Voici comment:

var request = new XMLHttpRequest();
request.open('GET', 'yourURL', false);  // `false` makes the request synchronous
request.send(null);

if (request.status === 200) {// That's HTTP for 'ok'
  console.log(request.responseText);
}

2. Code de restructuration

Laissez votre fonction accepter un rappel. Dans l'exemple, le code foopeut être fait pour accepter un rappel. Nous dirons à notre code comment réagir une fois footerminé.

Donc:

var result = foo();
// code that depends on `result` goes here

Devient:

foo(function(result) {
    // code that depends on `result`
});

Ici, nous avons transmis une fonction anonyme, mais nous pourrions tout aussi facilement transmettre une référence à une fonction existante, la faisant ressembler à:

function myHandler(result) {
    // code that depends on `result`
}
foo(myHandler);

Pour plus de détails sur la façon dont ce type de conception de rappel est effectué, consultez la réponse de Felix.

Maintenant, définissons foo lui-même pour agir en conséquence

function foo(callback) {
    var httpRequest = new XMLHttpRequest();
    httpRequest.onload = function(){ // when the request is loaded
       callback(httpRequest.responseText);// we're calling our method
    };
    httpRequest.open('GET', "/echo/json");
    httpRequest.send();
}

(violon)

Nous avons maintenant fait en sorte que notre fonction foo accepte une action à exécuter lorsque l'AJAX se termine avec succès, nous pouvons l'étendre davantage en vérifiant si l'état de la réponse n'est pas 200 et en agissant en conséquence (créez un gestionnaire d'échec, etc.). Résoudre efficacement notre problème.

Si vous avez encore du mal à comprendre cela, lisez le guide de démarrage AJAX sur MDN.

Benjamin Gruenbaum
la source
20
"les demandes synchrones bloquent l'exécution du code et peuvent fuir la mémoire et les événements" comment une demande synchrone peut-elle fuir la mémoire?
Matthew G
10
@MatthewG J'ai ajouté une prime à cette question , je vais voir ce que je peux pêcher. Je retire la citation de la réponse en attendant.
Benjamin Gruenbaum
17
Juste pour la référence, XHR 2 nous permet d'utiliser le onloadgestionnaire, qui ne se déclenche que lorsqu'il l' readyStateest 4. Bien sûr, il n'est pas pris en charge dans IE8. (iirc, peut avoir besoin de confirmation.)
Florian Margaine
9
Votre explication sur la façon de passer une fonction anonyme en tant que rappel est valide mais trompeuse. L'exemple var bar = foo (); demande qu'une variable soit définie, tandis que votre foo suggéré (functim () {}); ne définit pas la barre
Robbie Averill
396

XMLHttpRequest 2 (tout d'abord, lisez les réponses de Benjamin Gruenbaum & Felix Kling )

Si vous n'utilisez pas jQuery et que vous voulez un joli court XMLHttpRequest 2 qui fonctionne sur les navigateurs modernes et également sur les navigateurs mobiles, je suggère de l'utiliser de cette façon:

function ajax(a, b, c){ // URL, callback, just a placeholder
  c = new XMLHttpRequest;
  c.open('GET', a);
  c.onload = b;
  c.send()
}

Comme vous pouvez le voir:

  1. Il est plus court que toutes les autres fonctions répertoriées.
  2. Le rappel est défini directement (donc pas de fermetures inutiles supplémentaires).
  3. Il utilise la nouvelle onload (vous n'avez donc pas à vérifier l'état de readystate &&)
  4. Il y a d'autres situations dont je ne me souviens pas qui rendent le XMLHttpRequest 1 ennuyeux.

Il y a deux façons d'obtenir la réponse de cet appel Ajax (trois en utilisant le nom var XMLHttpRequest):

Le plus simple:

this.response

Ou si pour une raison quelconque vous bind()rappelez une classe:

e.target.response

Exemple:

function callback(e){
  console.log(this.response);
}
ajax('URL', callback);

Ou (celui ci-dessus est préférable, les fonctions anonymes sont toujours un problème):

ajax('URL', function(e){console.log(this.response)});

Rien de plus simple.

Maintenant, certaines personnes diront probablement qu'il vaut mieux utiliser onreadystatechange ou même le nom de variable XMLHttpRequest. C'est faux.

Check-out les fonctionnalités avancées de XMLHttpRequest

Il supportait tous les * navigateurs modernes. Et je peux confirmer que j'utilise cette approche car XMLHttpRequest 2 existe. Je n'ai jamais eu aucun type de problème sur tous les navigateurs que j'utilise.

onreadystatechange n'est utile que si vous souhaitez obtenir les en-têtes sur l'état 2.

L'utilisation du XMLHttpRequestnom de variable est une autre grosse erreur car vous devez exécuter le rappel dans les fermetures onload / oreadystatechange sinon vous l'avez perdu.


Maintenant, si vous voulez quelque chose de plus complexe en utilisant post et FormData, vous pouvez facilement étendre cette fonction:

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val},placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.send(d||null)
}

Encore une fois ... c'est une fonction très courte, mais elle obtient et publie.

Exemples d'utilisation:

x(url, callback); // By default it's get so no need to set
x(url, callback, 'post', {'key': 'val'}); // No need to set post data

Ou passez un élément de formulaire complet ( document.getElementsByTagName('form')[0]):

var fd = new FormData(form);
x(url, callback, 'post', fd);

Ou définissez des valeurs personnalisées:

var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);

Comme vous pouvez le voir, je n'ai pas implémenté la synchronisation ... c'est une mauvaise chose.

Cela dit ... pourquoi ne pas le faire facilement?


Comme mentionné dans le commentaire, l'utilisation de error && synchronous brise complètement le point de la réponse. Quel est le bon moyen d'utiliser Ajax correctement?

Gestionnaire d'erreurs

function x(a, b, e, d, c){ // URL, callback, method, formdata or {key:val}, placeholder
  c = new XMLHttpRequest;
  c.open(e||'get', a);
  c.onload = b;
  c.onerror = error;
  c.send(d||null)
}

function error(e){
  console.log('--Error--', this.type);
  console.log('this: ', this);
  console.log('Event: ', e)
}
function displayAjax(e){
  console.log(e, this);
}
x('WRONGURL', displayAjax);

Dans le script ci-dessus, vous avez un gestionnaire d'erreurs qui est défini de manière statique afin de ne pas compromettre la fonction. Le gestionnaire d'erreurs peut également être utilisé pour d'autres fonctions.

Mais pour vraiment sortir une erreur, la seule façon est d'écrire une mauvaise URL, auquel cas chaque navigateur renvoie une erreur.

Les gestionnaires d'erreurs peuvent être utiles si vous définissez des en-têtes personnalisés, définissez responseType sur un tampon de tableau d'objets blob ou autre ...

Même si vous passez 'POSTAPAPAP' comme méthode, cela ne générera pas d'erreur.

Même si vous passez 'fdggdgilfdghfldj' comme formdata, il ne générera pas d'erreur.

Dans le premier cas, l'erreur se trouve à l'intérieur du displayAjax()sous this.statusTextcomme Method not Allowed.

Dans le deuxième cas, cela fonctionne simplement. Vous devez vérifier côté serveur si vous avez transmis les bonnes données de publication.

cross-domain non autorisé lève automatiquement l'erreur.

Dans la réponse d'erreur, il n'y a aucun code d'erreur.

Il n'y a que le this.typequi est réglé sur erreur.

Pourquoi ajouter un gestionnaire d'erreurs si vous n'avez aucun contrôle sur les erreurs? La plupart des erreurs sont renvoyées à l'intérieur de cela dans la fonction de rappel displayAjax().

Donc: pas besoin de vérifier les erreurs si vous pouvez copier et coller l'URL correctement. ;)

PS: Lors du premier test, j'ai écrit x ('x', displayAjax) ..., et il a totalement obtenu une réponse ... ??? J'ai donc vérifié le dossier où se trouve le code HTML, et il y avait un fichier appelé 'x.xml'. Donc, même si vous oubliez l'extension de votre fichier, XMLHttpRequest 2 LE TROUVERA . J'ai lol


Lire un fichier synchrone

Ne fais pas ça.

Si vous souhaitez bloquer le navigateur pendant un certain temps, chargez un joli gros .txtfichier synchrone.

function omg(a, c){ // URL
  c = new XMLHttpRequest;
  c.open('GET', a, true);
  c.send();
  return c; // Or c.response
}

Maintenant vous pouvez faire

 var res = omg('thisIsGonnaBlockThePage.txt');

Il n'y a pas d'autre moyen de le faire de manière non asynchrone. (Ouais, avec la boucle setTimeout ... mais sérieusement?)

Un autre point est ... si vous travaillez avec des API ou tout simplement les fichiers de votre propre liste ou quoi que vous utilisiez toujours des fonctions différentes pour chaque demande ...

Seulement si vous avez une page où vous chargez toujours le même XML / JSON ou quoi que vous ayez besoin d'une seule fonction. Dans ce cas, modifiez un peu la fonction Ajax et remplacez b par votre fonction spéciale.


Les fonctions ci-dessus sont pour une utilisation de base.

Si vous souhaitez PROLONGER la fonction ...

Oui, vous pouvez.

J'utilise beaucoup d'API et l'une des premières fonctions que j'intègre dans chaque page HTML est la première fonction Ajax dans cette réponse, avec GET uniquement ...

Mais vous pouvez faire beaucoup de choses avec XMLHttpRequest 2:

J'ai fait un gestionnaire de téléchargement (en utilisant des plages des deux côtés avec CV, filereader, système de fichiers), divers convertisseurs de resizers d'image utilisant canvas, peupler les bases de données web SQL avec base64images et bien plus encore ... Mais dans ces cas, vous ne devez créer une fonction que pour cela but ... parfois vous avez besoin d'un blob, de tampons de tableau, vous pouvez définir des en-têtes, remplacer le mimetype et il y en a beaucoup plus ...

Mais la question ici est de savoir comment renvoyer une réponse Ajax ... (J'ai ajouté un moyen simple.)

cocco
la source
15
Bien que cette réponse soit agréable (et nous aimons tous XHR2 et la publication de données de fichiers et de données en plusieurs parties est totalement géniale) - cela montre du sucre syntaxique pour publier XHR avec JavaScript - vous voudrez peut-être mettre cela dans un article de blog (je l'aimerais) ou même dans une bibliothèque (pas sûr du nom x, ajaxou xhrpeut-être plus joli :)). Je ne vois pas comment cela résout le retour de la réponse d'un appel AJAX. (quelqu'un pourrait encore faire var res = x("url")et ne pas comprendre pourquoi cela ne fonctionne pas;)). Sur une note latérale - ce serait cool si vous reveniez cde la méthode afin que les utilisateurs puissent se connecter erroretc.
Benjamin Gruenbaum
25
2.ajax is meant to be async.. so NO var res=x('url')..C'est tout l'intérêt de cette question et des réponses :)
Benjamin Gruenbaum
3
pourquoi y a-t-il un paramètre «c» dans les fonctions, si sur la première ligne vous écrasez la valeur qu'il avait? est-ce que je manque quelque chose?
Brian H.
2
Vous pouvez utiliser des paramètres comme espace réservé pour éviter d'écrire plusieurs fois "var"
cocco
11
@cocco Vous avez donc écrit du code trompeur et illisible dans une réponse SO afin d'économiser quelques frappes? S'il vous plaît ne faites pas ça.
pierre
316

Si vous utilisez des promesses, cette réponse est pour vous.

Cela signifie AngularJS, jQuery (avec différé), le remplacement natif de XHR (récupération), EmberJS, la sauvegarde de BackboneJS ou toute bibliothèque de nœuds qui renvoie des promesses.

Votre code devrait être quelque chose dans le sens de ceci:

function foo() {
    var data;
    // or $.get(...).then, or request(...).then, or query(...).then
    fetch("/echo/json").then(function(response){
        data = response.json();
    });
    return data;
}

var result = foo(); // result is always undefined no matter what.

Felix Kling a fait un excellent travail en écrivant une réponse pour les personnes utilisant jQuery avec des rappels pour AJAX. J'ai une réponse pour XHR natif. Cette réponse concerne l'utilisation générique des promesses sur le frontend ou le backend.


La question centrale

Le modèle de concurrence JavaScript dans le navigateur et sur le serveur avec NodeJS / io.js est asynchrone et réactif .

Chaque fois que vous appelez une méthode qui renvoie une promesse, les thengestionnaires sont toujours exécutés de manière asynchrone - c'est-à-dire après le code en dessous d'eux qui n'est pas dans un.then gestionnaire.

Cela signifie que lorsque vous retournez datalethen gestionnaire que vous avez défini ne s'est pas encore exécuté. Cela signifie à son tour que la valeur que vous retournez n'a pas été définie à la bonne valeur dans le temps.

Voici une analogie simple pour le problème:

    function getFive(){
        var data;
        setTimeout(function(){ // set a timer for one second in the future
           data = 5; // after a second, do this
        }, 1000);
        return data;
    }
    document.body.innerHTML = getFive(); // `undefined` here and not 5

La valeur de dataest undefineddepuis ledata = 5 partie n'a pas encore été exécutée. Il s'exécutera probablement dans une seconde, mais à ce moment-là, il n'est plus pertinent pour la valeur renvoyée.

Étant donné que l'opération n'a pas encore eu lieu (AJAX, appel serveur, IO, minuterie), vous retournez la valeur avant que la demande n'ait la possibilité de dire à votre code quelle est cette valeur.

Une solution possible à ce problème consiste à coder de manière réactive , en indiquant à votre programme quoi faire une fois le calcul terminé. Les promesses permettent activement cela en étant de nature temporelle (sensible au temps).

Récapitulation rapide des promesses

Une promesse est une valeur dans le temps . Les promesses ont un état, elles commencent comme en attente sans valeur et peuvent se régler pour:

  • remplie, ce qui signifie que le calcul s'est terminé avec succès.
  • rejeté, ce qui signifie que le calcul a échoué.

Une promesse ne peut changer d'état qu'une seule fois, après quoi elle restera toujours dans le même état pour toujours. Vous pouvez associer des thengestionnaires à des promesses pour extraire leur valeur et gérer les erreurs. thenles gestionnaires permettent d' enchaîner les appels. Les promesses sont créées à l' aide d'API qui les renvoient . Par exemple, le remplacement AJAX plus moderne fetchou jQuery$.get promesses de retour .

Lorsque nous faisons appel .thenà une promesse et que nous en retournons quelque chose, nous obtenons une promesse pour la valeur traitée . Si nous rendons une autre promesse, nous aurons des choses incroyables, mais tenons nos chevaux.

Avec des promesses

Voyons comment nous pouvons résoudre le problème ci-dessus avec des promesses. Tout d'abord, démontrons notre compréhension des états promis d'en haut en utilisant le constructeur Promise pour créer une fonction de retard:

function delay(ms){ // takes amount of milliseconds
    // returns a new promise
    return new Promise(function(resolve, reject){
        setTimeout(function(){ // when the time is up
            resolve(); // change the promise to the fulfilled state
        }, ms);
    });
}

Maintenant, après avoir converti setTimeout pour utiliser des promesses, nous pouvons utiliser thenpour le faire compter:

function delay(ms){ // takes amount of milliseconds
  // returns a new promise
  return new Promise(function(resolve, reject){
    setTimeout(function(){ // when the time is up
      resolve(); // change the promise to the fulfilled state
    }, ms);
  });
}

function getFive(){
  // we're RETURNING the promise, remember, a promise is a wrapper over our value
  return delay(100).then(function(){ // when the promise is ready
      return 5; // return the value 5, promises are all about return values
  })
}
// we _have_ to wrap it like this in the call site, we can't access the plain value
getFive().then(function(five){ 
   document.body.innerHTML = five;
});

Fondamentalement, au lieu de renvoyer une valeur que nous ne pouvons pas faire à cause du modèle de concurrence - nous retournons un wrapper pour une valeur que nous pouvons dérouler avec then. C'est comme une boîte avec laquelle vous pouvez ouvrir then.

Appliquer cela

Cela est le même pour votre appel d'API d'origine, vous pouvez:

function foo() {
    // RETURN the promise
    return fetch("/echo/json").then(function(response){
        return response.json(); // process it inside the `then`
    });
}

foo().then(function(response){
    // access the value inside the `then`
})

Donc ça marche aussi bien. Nous avons appris que nous ne pouvons pas renvoyer des valeurs d'appels déjà asynchrones, mais nous pouvons utiliser des promesses et les enchaîner pour effectuer le traitement. Nous savons maintenant comment renvoyer la réponse d'un appel asynchrone.

ES2015 (ES6)

ES6 introduit des générateurs qui sont des fonctions qui peuvent revenir au milieu puis reprendre le point où elles étaient. Ceci est généralement utile pour les séquences, par exemple:

function* foo(){ // notice the star, this is ES6 so new browsers/node/io only
    yield 1;
    yield 2;
    while(true) yield 3;
}

Est une fonction qui renvoie un itérateur sur la séquence 1,2,3,3,3,3,....qui peut être itérée. Bien que cela soit intéressant en soi et ouvre de nombreuses possibilités, il existe un cas particulier intéressant.

Si la séquence que nous produisons est une séquence d'actions plutôt que des nombres - nous pouvons mettre la fonction en pause chaque fois qu'une action est produite et l'attendre avant de reprendre la fonction. Donc, au lieu d'une séquence de nombres, nous avons besoin d'une séquence d' avenir valeurs - c'est-à-dire: des promesses.

Cette astuce quelque peu délicate mais très puissante nous permet d'écrire du code asynchrone de manière synchrone. Il y a plusieurs "coureurs" qui font cela pour vous, en écrire une est quelques courtes lignes de code mais dépasse le cadre de cette réponse. J'utiliserai Bluebird Promise.coroutineici, mais il y a d'autres emballages comme coou Q.async.

var foo = coroutine(function*(){
    var data = yield fetch("/echo/json"); // notice the yield
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
});

Cette méthode renvoie une promesse elle-même, que nous pouvons consommer à partir d'autres coroutines. Par exemple:

var main = coroutine(function*(){
   var bar = yield foo(); // wait our earlier coroutine, it returns a promise
   // server call done here, code below executes when done
   var baz = yield fetch("/api/users/"+bar.userid); // depends on foo's result
   console.log(baz); // runs after both requests done
});
main();

ES2016 (ES7)

Dans ES7, c'est encore plus standardisé, il y a plusieurs propositions en ce moment mais dans toutes, vous pouvez le awaitpromettre. Ceci est juste du "sucre" (syntaxe plus agréable) pour la proposition ES6 ci-dessus en ajoutant les mots async- awaitclés et . Faire l'exemple ci-dessus:

async function foo(){
    var data = await fetch("/echo/json"); // notice the await
    // code here only executes _after_ the request is done
    return data.json(); // data is defined
}

Cela renvoie quand même une promesse :)

Benjamin Gruenbaum
la source
Ce devrait être la réponse acceptée. +1 pour asynchrone / attendre (mais ne le devrions-nous pas return await data.json();?)
Lewis Donovan
247

Vous utilisez Ajax de manière incorrecte. L'idée n'est pas de lui faire retourner quoi que ce soit, mais plutôt de transmettre les données à quelque chose appelé une fonction de rappel, qui gère les données.

C'est:

function handleData( responseData ) {

    // Do what you want with the data
    console.log(responseData);
}

$.ajax({
    url: "hi.php",
    ...
    success: function ( data, status, XHR ) {
        handleData(data);
    }
});

Renvoyer quoi que ce soit dans le gestionnaire de soumission ne fera rien. Vous devez au lieu de cela remettre les données ou faire ce que vous voulez directement avec elles dans la fonction de réussite.

Nic
la source
13
Cette réponse est complètement sémantique ... votre méthode de réussite n'est qu'un rappel dans un rappel. Vous pourriez juste avoir success: handleDataet cela fonctionnerait.
Jacques ジ ャ ッ ク
5
Et si vous voulez retourner le "responseData" en dehors de "handleData" ... :) ... comment allez-vous le faire ...? ... car un simple retour le ramènera au rappel "succès" de l'ajax ... et pas en dehors de "handleData" ...
pesho hristov
@Jacques & @pesho hristov Vous avez manqué ce point. Soumettre le gestionnaire n'est pas la successméthode, c'est la portée environnante de $.ajax.
travnik
@travnik Je n'ai pas raté ça. Si vous preniez le contenu de handleData et le mettiez dans la méthode du succès, cela agirait exactement de la même manière ...
Jacques ジ ャ ッ ク
234

La solution la plus simple consiste à créer une fonction JavaScript et à l'appeler pour le successrappel Ajax .

function callServerAsync(){
    $.ajax({
        url: '...',
        success: function(response) {

            successCallback(response);
        }
    });
}

function successCallback(responseObj){
    // Do something like read the response and show data
    alert(JSON.stringify(responseObj)); // Only applicable to JSON response
}

function foo(callback) {

    $.ajax({
        url: '...',
        success: function(response) {
           return callback(null, response);
        }
    });
}

var result = foo(function(err, result){
          if (!err)
           console.log(result);    
}); 
Hemant Bavle
la source
3
Je ne sais pas qui l'a voté négatif. Mais c'est un travail qui a fonctionné en fait j'ai utilisé cette approche pour créer une application entière. Le jquery.ajax ne renvoie pas de données, il est donc préférable d'utiliser l'approche ci-dessus. Si c'est faux, veuillez expliquer et suggérer une meilleure façon de le faire.
Hemant Bavle
11
Désolé, j'ai oublié de laisser un commentaire (je fais d'habitude!). Je l'ai rétrogradé. Les votes à la baisse n'indiquent pas l'exactitude factuelle ou le manque de, ils indiquent l'utilité dans le contexte ou le manque de. Je ne trouve pas votre réponse utile étant donné celle de Felix qui l'explique déjà plus en détail. Sur une note latérale, pourquoi voudriez-vous stringifier la réponse si c'est JSON?
Benjamin Gruenbaum
5
ok .. @Benjamin j'ai utilisé stringify, pour convertir un objet JSON en chaîne. Et merci d'avoir clarifié votre point. Gardera à l'esprit de poster des réponses plus élaborées.
Hemant Bavle
Et si vous voulez retourner le "responseObj" en dehors de "successCallback" ... :) ... comment allez-vous faire ...? ... car un simple retour le ramènera au rappel "succès" de l'ajax ... et pas en dehors de "successCallback" ...
pesho hristov
221

Je répondrai avec une bande dessinée à la main horrible. La deuxième image est la raison pour laquelle resultest undefineddans votre exemple de code.

entrez la description de l'image ici

Johannes Fahrenkrug
la source
32
Une image vaut mille mots , personne A - demande à la personne B les détails pour réparer sa voiture, à son tour la personne B - fait un appel Ajax et attend la réponse du serveur pour les détails de réparation de la voiture, lorsque la réponse est reçue, la fonction Ajax Success appelle la personne La fonction B lui transmet la réponse comme argument, la personne A reçoit la réponse.
shaijut
10
Ce serait génial si vous ajoutiez des lignes de code avec chaque image pour illustrer les concepts.
Hassan Baig
1
Pendant ce temps, le gars avec la voiture est coincé sur le bord de la route. Il exige que la voiture soit réparée avant de continuer. Il est maintenant seul sur le bord de la route à attendre ... Il préfèrerait être au téléphone à attendre des changements de statut mais le mécanicien ne le ferait pas ... Le mécanicien a dit qu'il devait continuer son travail et ne pouvait pas traînez simplement au téléphone. Le mécanicien a promis qu'il le rappellerait dès qu'il le pourrait. Après environ 4 heures, le gars abandonne et appelle Uber. - Exemple de timeout.
barrypicker
@barrypicker :-D Brilliant!
Johannes Fahrenkrug
159

Angulaire1

Pour les personnes qui utilisent AngularJS , peuvent gérer cette situation en utilisant Promises.

Ici, il est dit,

Les promesses peuvent être utilisées pour supprimer les fonctions asynchrones et permettre de chaîner plusieurs fonctions ensemble.

Vous pouvez également trouver une belle explication ici .

Exemple trouvé dans les documents mentionnés ci-dessous.

  promiseB = promiseA.then(
    function onSuccess(result) {
      return result + 1;
    }
    ,function onError(err) {
      //Handle error
    }
  );

 // promiseB will be resolved immediately after promiseA is resolved 
 // and its value will be the result of promiseA incremented by 1.

Angular2 et versions ultérieures

En Angular2regardant l'exemple suivant, mais il est recommandé de l'utiliser Observablesavec Angular2.

 search(term: string) {
     return this.http
  .get(`https://api.spotify.com/v1/search?q=${term}&type=artist`)
  .map((response) => response.json())
  .toPromise();

}

Vous pouvez consommer cela de cette façon,

search() {
    this.searchService.search(this.searchField.value)
      .then((result) => {
    this.result = result.artists.items;
  })
  .catch((error) => console.error(error));
}

Voir le post original ici. Mais Typescript ne prend pas en charge les promesses natives d'es6 , si vous voulez l'utiliser, vous pourriez avoir besoin d'un plugin pour cela.

De plus, voici les promesses spécifiées ici.

Maleen Abewardana
la source
15
Cela n'explique cependant pas comment les promesses résoudraient ce problème.
Benjamin Gruenbaum
4
Les méthodes jQuery et fetch renvoient également des promesses. Je suggère de réviser votre réponse. Bien que jQuery ne soit pas tout à fait le même (alors il est là, mais le catch ne l'est pas).
Tracker1
153

La plupart des réponses ici donnent des suggestions utiles lorsque vous avez une seule opération asynchrone, mais parfois, cela se produit lorsque vous devez effectuer une opération asynchrone pour chaque entrée d'un tableau ou d'une autre structure de type liste. La tentation est de faire ceci:

// WRONG
var results = [];
theArray.forEach(function(entry) {
    doSomethingAsync(entry, function(result) {
        results.push(result);
    });
});
console.log(results); // E.g., using them, returning them, etc.

Exemple:

La raison pour laquelle cela ne fonctionne pas est que les rappels de doSomethingAsyncn'ont pas encore été exécutés au moment où vous essayez d'utiliser les résultats.

Donc, si vous avez un tableau (ou une liste quelconque) et que vous souhaitez effectuer des opérations asynchrones pour chaque entrée, vous avez deux options: effectuer les opérations en parallèle (chevauchement) ou en série (l'une après l'autre en séquence).

Parallèle

Vous pouvez tous les démarrer et garder une trace du nombre de rappels que vous attendez, puis utiliser les résultats lorsque vous avez reçu autant de rappels:

var results = [];
var expecting = theArray.length;
theArray.forEach(function(entry, index) {
    doSomethingAsync(entry, function(result) {
        results[index] = result;
        if (--expecting === 0) {
            // Done!
            console.log("Results:", results); // E.g., using the results
        }
    });
});

Exemple:

(Nous pourrions supprimer expectinget simplement utiliser results.length === theArray.length, mais cela nous laisse ouverts à la possibilité de theArraychanger pendant que les appels sont en attente ...)

Remarquez comment nous utilisons le indexfrom forEachpour enregistrer le résultat dans resultsla même position que l'entrée à laquelle il se rapporte, même si les résultats arrivent dans le désordre (car les appels asynchrones ne se terminent pas nécessairement dans l'ordre dans lequel ils ont été démarrés).

Mais que faire si vous devez renvoyer ces résultats à partir d'une fonction? Comme les autres réponses l'ont souligné, vous ne pouvez pas; vous devez avoir votre fonction accepter et appeler un rappel (ou retourner une promesse ). Voici une version de rappel:

function doSomethingWith(theArray, callback) {
    var results = [];
    var expecting = theArray.length;
    theArray.forEach(function(entry, index) {
        doSomethingAsync(entry, function(result) {
            results[index] = result;
            if (--expecting === 0) {
                // Done!
                callback(results);
            }
        });
    });
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

Exemple:

Ou voici une version renvoyant un à la Promiseplace:

function doSomethingWith(theArray) {
    return new Promise(function(resolve) {
        var results = [];
        var expecting = theArray.length;
        theArray.forEach(function(entry, index) {
            doSomethingAsync(entry, function(result) {
                results[index] = result;
                if (--expecting === 0) {
                    // Done!
                    resolve(results);
                }
            });
        });
    });
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Bien sûr, si doSomethingAsyncnous transmettions des erreurs, nous utiliserions rejectpour rejeter la promesse lorsque nous aurons une erreur.)

Exemple:

(Ou alternativement, vous pouvez faire un wrapper pour doSomethingAsyncque retourne une promesse, puis faites ce qui suit ...)

Si doSomethingAsyncvous donne une promesse , vous pouvez utiliser Promise.all:

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(function(entry) {
        return doSomethingAsync(entry);
    }));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Si vous savez que doSomethingAsynccela ignorera un deuxième et un troisième argument, vous pouvez simplement le passer directement à map( mapappelle son rappel avec trois arguments, mais la plupart des gens n'utilisent que le premier la plupart du temps):

function doSomethingWith(theArray) {
    return Promise.all(theArray.map(doSomethingAsync));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Exemple:

Notez que Promise.allrésout sa promesse avec un tableau des résultats de toutes les promesses que vous lui faites quand elles sont toutes résolues, ou rejette sa promesse lorsque la première des promesses que vous lui donnez rejette.

Séries

Supposons que vous ne vouliez pas que les opérations soient en parallèle? Si vous souhaitez les exécuter l'une après l'autre, vous devez attendre la fin de chaque opération avant de commencer la suivante. Voici un exemple d'une fonction qui fait cela et appelle un rappel avec le résultat:

function doSomethingWith(theArray, callback) {
    var results = [];
    doOne(0);
    function doOne(index) {
        if (index < theArray.length) {
            doSomethingAsync(theArray[index], function(result) {
                results.push(result);
                doOne(index + 1);
            });
        } else {
            // Done!
            callback(results);
        }
    }
}
doSomethingWith(theArray, function(results) {
    console.log("Results:", results);
});

(Puisque nous effectuons le travail en série, nous pouvons simplement l'utiliser results.push(result)car nous savons que nous n'obtiendrons pas de résultats en désordre. Dans ce qui précède, nous aurions pu utiliser results[index] = result;, mais dans certains des exemples suivants, nous n'avons pas d'index utiliser.)

Exemple:

(Ou, encore une fois, créez un wrapper pour doSomethingAsynccela vous donne une promesse et faites ce qui suit ...)

Si doSomethingAsyncvous donne une promesse, si vous pouvez utiliser la syntaxe ES2017 + (peut-être avec un transpilateur comme Babel ), vous pouvez utiliser une asyncfonction avecfor-of et await:

async function doSomethingWith(theArray) {
    const results = [];
    for (const entry of theArray) {
        results.push(await doSomethingAsync(entry));
    }
    return results;
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Exemple:

Si vous ne pouvez pas encore utiliser la syntaxe ES2017 +, vous pouvez utiliser une variante de la modèle "Promise réduire" (c'est plus complexe que la promesse habituelle de réduire car nous ne transmettons pas le résultat de l'un à l'autre, mais à la place regroupant leurs résultats dans un tableau):

function doSomethingWith(theArray) {
    return theArray.reduce(function(p, entry) {
        return p.then(function(results) {
            return doSomethingAsync(entry).then(function(result) {
                results.push(result);
                return results;
            });
        });
    }, Promise.resolve([]));
}
doSomethingWith(theArray).then(function(results) {
    console.log("Results:", results);
});

Exemple:

... ce qui est moins encombrant avec fonctions flèches ES2015 + :

function doSomethingWith(theArray) {
    return theArray.reduce((p, entry) => p.then(results => doSomethingAsync(entry).then(result => {
        results.push(result);
        return results;
    })), Promise.resolve([]));
}
doSomethingWith(theArray).then(results => {
    console.log("Results:", results);
});

Exemple:

TJ Crowder
la source
1
Pourriez-vous expliquer comment fonctionne la if (--expecting === 0)partie du code s'il vous plaît? La version de rappel de votre solution fonctionne très bien pour moi, je ne comprends tout simplement pas comment, avec cette déclaration, vous vérifiez le nombre de réponses complétées. Je comprends que c'est juste un manque de connaissances de ma part. Y a-t-il une autre façon de faire un chèque?
Sarah
@Sarah: expectingcommence par la valeur de array.length, qui est le nombre de demandes que nous allons faire. Nous savons que le rappel ne sera pas appelé tant que toutes ces demandes n'auront pas été lancées. Dans le rappel, if (--expecting === 0)fait ceci: 1. Décrémentations expecting(nous avons reçu une réponse, donc nous attendons une réponse de moins) et si la valeur après la décrémentation est 0 (nous n'attendons plus de réponses), nous sommes terminé!
TJ Crowder
1
@PatrickRoberts - Merci !! Oui, erreur de copier-coller, ce deuxième argument a été complètement ignoré dans cet exemple (qui est la seule raison pour laquelle il n'a pas échoué, car comme vous l'avez souligné, resultsn'existait pas). :-) Corrigé.
TJ Crowder
111

Jetez un œil à cet exemple:

var app = angular.module('plunker', []);

app.controller('MainCtrl', function($scope,$http) {

    var getJoke = function(){
        return $http.get('http://api.icndb.com/jokes/random').then(function(res){
            return res.data.value;  
        });
    }

    getJoke().then(function(res) {
        console.log(res.joke);
    });
});

Comme vous pouvez le voir getJokeest le retour d' une résolution promesse (il est résolu lors du retour res.data.value). Vous attendez donc que la requête $ http.get soit terminée, puis que console.log (res.joke) soit exécuté (comme un flux asynchrone normal).

C'est le plnkr:

http://embed.plnkr.co/XlNR7HpCaIhJxskMJfSg/

Chemin ES6 (async - attendre)

(function(){
  async function getJoke(){
    let response = await fetch('http://api.icndb.com/jokes/random');
    let data = await response.json();
    return data.value;
  }

  getJoke().then((joke) => {
    console.log(joke);
  });
})();
Francisco Carmona
la source
107

C'est l'un des endroits où la liaison de données ou le concept de magasin utilisé dans de nombreux nouveaux frameworks JavaScript fonctionnera très bien pour vous ...

Donc, si vous utilisez Angular, React ou tout autre framework qui utilise le concept de liaison de données ou de stockage de deux manières, ce problème est simplement résolu pour vous, donc en un mot, votre résultat est undefinedà la première étape, donc vous avez obtenu result = undefinedavant de recevoir le données, puis dès que vous obtenez le résultat, il sera mis à jour et affecté à la nouvelle valeur quelle réponse de votre appel Ajax ...

Mais comment vous pouvez le faire en pur javascript ou jQuery par exemple comme vous l'avez demandé dans cette question?

Vous pouvez utiliser un rappel , une promesse et récemment observable pour le gérer pour vous, par exemple dans les promesses, nous avons une fonction comme success()ou then()qui sera exécutée lorsque vos données seront prêtes pour vous, de même avec la fonction de rappel ou d' abonnement sur observable .

Par exemple, dans votre cas où vous utilisez jQuery , vous pouvez faire quelque chose comme ceci:

$(document).ready(function(){
    function foo() {
        $.ajax({url: "api/data", success: function(data){
            fooDone(data); //after we have data, we pass it to fooDone
        }});
    };

    function fooDone(data) {
        console.log(data); //fooDone has the data and console.log it
    };

    foo(); //call happens here
});

Pour plus d'informations, étudiez les promesses et les observables qui sont de nouvelles façons de faire ces trucs asynchrones.

Alireza
la source
C'est très bien à l'échelle mondiale, mais dans certains contextes de module, vous voulez probablement assurer le bon contexte pour le rappel, par exemple$.ajax({url: "api/data", success: fooDone.bind(this)});
steve.sims
8
Ceci est en fait incorrect car React est une liaison de données à sens unique
Matthew Brent
@MatthewBrent vous n'avez pas tort, mais pas aussi, les accessoires React sont des objets et s'ils sont modifiés, ils changent tout au long de l'application, mais ce n'est pas une façon que le développeur React recommande de l'utiliser ...
Alireza
98

C'est un problème très courant auquel nous sommes confrontés alors que nous luttons avec les «mystères» de JavaScript. Permettez-moi d'essayer de démystifier ce mystère aujourd'hui.

Commençons par une simple fonction JavaScript:

function foo(){
// do something 
 return 'wohoo';
}

let bar = foo(); // bar is 'wohoo' here

C'est un simple appel de fonction synchrone (où chaque ligne de code est «terminée avec son travail» avant la suivante dans l'ordre), et le résultat est le même que prévu.

Ajoutons maintenant un peu de torsion, en introduisant peu de retard dans notre fonction, afin que toutes les lignes de code ne soient pas «finies» dans l'ordre. Ainsi, il émulera le comportement asynchrone de la fonction:

function foo(){
 setTimeout( ()=>{
   return 'wohoo';
  }, 1000 )
}

let bar = foo() // bar is undefined here

Alors voilà, ce retard vient de casser la fonctionnalité que nous attendions! Mais que s'est-il passé exactement? Eh bien, c'est en fait assez logique si vous regardez le code. la fonction foo(), lors de son exécution, ne retourne rien (ainsi la valeur retournée est undefined), mais elle démarre un temporisateur, qui exécute une fonction après 1s pour retourner 'wohoo'. Mais comme vous pouvez le voir, la valeur attribuée à la barre est la substance immédiatement renvoyée par foo (), qui n'est rien, c'est-à-dire juste undefined.

Alors, comment abordons-nous ce problème?

Demandons à notre fonction une PROMESSE . La promesse est vraiment ce qu'elle signifie: cela signifie que la fonction vous garantit de fournir toute sortie qu'elle obtiendra à l'avenir. voyons donc cela en action pour notre petit problème ci-dessus:

function foo(){
   return new Promise( (resolve, reject) => { // I want foo() to PROMISE me something
    setTimeout ( function(){ 
      // promise is RESOLVED , when execution reaches this line of code
       resolve('wohoo')// After 1 second, RESOLVE the promise with value 'wohoo'
    }, 1000 )
  })
}

let bar ; 
foo().then( res => {
 bar = res;
 console.log(bar) // will print 'wohoo'
});

Ainsi, le résumé est - pour aborder les fonctions asynchrones comme les appels basés sur ajax, etc., vous pouvez utiliser une promesse de resolvevaleur (que vous avez l'intention de renvoyer). Ainsi, en bref, vous résolvez la valeur au lieu de retourner , dans les fonctions asynchrones.

MISE À JOUR (promesses avec async / attente)

Outre l'utilisation then/catchpour travailler avec des promesses, il existe une autre approche. L'idée est de reconnaître une fonction asynchrone , puis d' attendre que les promesses soient résolues, avant de passer à la ligne de code suivante. C'est toujours promisessous le capot, mais avec une approche syntaxique différente. Pour rendre les choses plus claires, vous pouvez trouver une comparaison ci-dessous:

puis / catch version:

function saveUsers(){
     getUsers()
      .then(users => {
         saveSomewhere(users);
      })
      .catch(err => {
          console.error(err);
       })
 }

version asynchrone / en attente:

  async function saveUsers(){
     try{
        let users = await getUsers()
        saveSomewhere(users);
     }
     catch(err){
        console.error(err);
     }
  }
Anish K.
la source
est-ce toujours considéré comme le meilleur moyen de renvoyer une valeur à partir d'une promesse ou d'une asynchronisation / attente?
edwardsmarkf
3
@edwardsmarkf Personnellement, je ne pense pas qu'il existe un meilleur moyen en tant que tel. J'utilise des promesses avec then / catch, async / wait ainsi que des générateurs pour les parties asynchrones de mon code. Cela dépend largement du contexte d'utilisation.
Anish K.
96

Une autre approche pour renvoyer une valeur à partir d'une fonction asynchrone consiste à passer un objet qui stockera le résultat de la fonction asynchrone.

Voici un exemple de la même:

var async = require("async");

// This wires up result back to the caller
var result = {};
var asyncTasks = [];
asyncTasks.push(function(_callback){
    // some asynchronous operation
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;
            _callback();
        }
    });
});

async.parallel(asyncTasks, function(){
    // result is available after performing asynchronous operation
    console.log(result)
    console.log('Done');
});

J'utilise le result objet pour stocker la valeur pendant l'opération asynchrone. Cela permet au résultat d'être disponible même après le travail asynchrone.

J'utilise beaucoup cette approche. Je serais intéressé de savoir dans quelle mesure cette approche fonctionne lorsque le câblage du résultat via des modules consécutifs est impliqué.

jsbisht
la source
9
Il n'y a rien de spécial à utiliser un objet ici. Cela fonctionnerait également si vous affectiez directement la réponse à result. Cela fonctionne car vous lisez la variable une fois la fonction asynchrone terminée.
Felix Kling
85

Bien que les promesses et les rappels fonctionnent bien dans de nombreuses situations, il est difficile d'exprimer quelque chose comme:

if (!name) {
  name = async1();
}
async2(name);

Vous finiriez par passer async1; vérifiez s'il namen'est pas défini ou non et appelez le rappel en conséquence.

async1(name, callback) {
  if (name)
    callback(name)
  else {
    doSomething(callback)
  }
}

async1(name, async2)

Bien que cela soit correct dans de petits exemples, cela devient ennuyeux lorsque de nombreux cas similaires et une gestion des erreurs sont impliqués.

Fibers aide à résoudre le problème.

var Fiber = require('fibers')

function async1(container) {
  var current = Fiber.current
  var result
  doSomething(function(name) {
    result = name
    fiber.run()
  })
  Fiber.yield()
  return result
}

Fiber(function() {
  var name
  if (!name) {
    name = async1()
  }
  async2(name)
  // Make any number of async calls from here
}

Vous pouvez consulter le projet ici .

rohithpr
la source
1
@recurf - Ce n'est pas mon projet. Vous pouvez essayer d'utiliser leur outil de suivi des problèmes.
rohithpr
1
est-ce similaire aux fonctions du générateur? developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/… *
Emanegux
1
Est-ce toujours pertinent?
Aluan Haddad
Vous pouvez utiliser async-awaitsi vous utilisez certaines des dernières versions de node. Si quelqu'un est bloqué avec des versions plus anciennes, il peut utiliser cette méthode.
rohithpr
83

L'exemple suivant que j'ai écrit montre comment

  • Gérer les appels HTTP asynchrones;
  • Attendez la réponse de chaque appel d'API;
  • Utilisez le modèle Promise ;
  • Utilisez le modèle Promise.all pour joindre plusieurs appels HTTP;

Cet exemple de travail est autonome. Il définira un objet de requête simple qui utilise l' XMLHttpRequestobjet window pour effectuer des appels. Il définira une fonction simple pour attendre qu'un tas de promesses soient terminées.

Le contexte. L'exemple interroge le point de terminaison de l' API Web Spotify afin de rechercher des playlistobjets pour un ensemble donné de chaînes de requête:

[
 "search?type=playlist&q=%22doom%20metal%22",
 "search?type=playlist&q=Adele"
]

Pour chaque élément, une nouvelle promesse déclenchera un bloc - ExecutionBlock, analysera le résultat, planifiera un nouvel ensemble de promesses basé sur le tableau de résultats, c'est-à-dire une liste d' userobjets Spotify et exécutera le nouvel appel HTTP de ExecutionProfileBlockmanière asynchrone.

Vous pouvez alors voir une structure Promise imbriquée, qui vous permet de générer plusieurs appels HTTP imbriqués complètement et asynchrones, et de joindre les résultats de chaque sous-ensemble d'appels Promise.all.

REMARQUE Les searchAPI Spotify récentes nécessiteront un jeton d'accès à spécifier dans les en-têtes de demande:

-H "Authorization: Bearer {your access token}" 

Ainsi, pour exécuter l'exemple suivant, vous devez placer votre jeton d'accès dans les en-têtes de demande:

var spotifyAccessToken = "YourSpotifyAccessToken";
var console = {
    log: function(s) {
        document.getElementById("console").innerHTML += s + "<br/>"
    }
}

// Simple XMLHttpRequest
// based on https://davidwalsh.name/xmlhttprequest
SimpleRequest = {
    call: function(what, response) {
        var request;
        if (window.XMLHttpRequest) { // Mozilla, Safari, ...
            request = new XMLHttpRequest();
        } else if (window.ActiveXObject) { // Internet Explorer
            try {
                request = new ActiveXObject('Msxml2.XMLHTTP');
            }
            catch (e) {
                try {
                  request = new ActiveXObject('Microsoft.XMLHTTP');
                } catch (e) {}
            }
        }

        // State changes
        request.onreadystatechange = function() {
            if (request.readyState === 4) { // Done
                if (request.status === 200) { // Complete
                    response(request.responseText)
                }
                else
                    response();
            }
        }
        request.open('GET', what, true);
        request.setRequestHeader("Authorization", "Bearer " + spotifyAccessToken);
        request.send(null);
    }
}

//PromiseAll
var promiseAll = function(items, block, done, fail) {
    var self = this;
    var promises = [],
                   index = 0;
    items.forEach(function(item) {
        promises.push(function(item, i) {
            return new Promise(function(resolve, reject) {
                if (block) {
                    block.apply(this, [item, index, resolve, reject]);
                }
            });
        }(item, ++index))
    });
    Promise.all(promises).then(function AcceptHandler(results) {
        if (done) done(results);
    }, function ErrorHandler(error) {
        if (fail) fail(error);
    });
}; //promiseAll

// LP: deferred execution block
var ExecutionBlock = function(item, index, resolve, reject) {
    var url = "https://api.spotify.com/v1/"
    url += item;
    console.log( url )
    SimpleRequest.call(url, function(result) {
        if (result) {

            var profileUrls = JSON.parse(result).playlists.items.map(function(item, index) {
                return item.owner.href;
            })
            resolve(profileUrls);
        }
        else {
            reject(new Error("call error"));
        }
    })
}

arr = [
    "search?type=playlist&q=%22doom%20metal%22",
    "search?type=playlist&q=Adele"
]

promiseAll(arr, function(item, index, resolve, reject) {
    console.log("Making request [" + index + "]")
    ExecutionBlock(item, index, resolve, reject);
}, function(results) { // Aggregated results

    console.log("All profiles received " + results.length);
    //console.log(JSON.stringify(results[0], null, 2));

    ///// promiseall again

    var ExecutionProfileBlock = function(item, index, resolve, reject) {
        SimpleRequest.call(item, function(result) {
            if (result) {
                var obj = JSON.parse(result);
                resolve({
                    name: obj.display_name,
                    followers: obj.followers.total,
                    url: obj.href
                });
            } //result
        })
    } //ExecutionProfileBlock

    promiseAll(results[0], function(item, index, resolve, reject) {
        //console.log("Making request [" + index + "] " + item)
        ExecutionProfileBlock(item, index, resolve, reject);
    }, function(results) { // aggregated results
        console.log("All response received " + results.length);
        console.log(JSON.stringify(results, null, 2));
    }

    , function(error) { // Error
        console.log(error);
    })

    /////

  },
  function(error) { // Error
      console.log(error);
  });
<div id="console" />

J'ai longuement discuté de cette solution ici .

loretoparisi
la source
80

En bref, vous devez implémenter un rappel comme celui-ci:

function callback(response) {
    // Here you can do what ever you want with the response object.
    console.log(response);
}

$.ajax({
    url: "...",
    success: callback
});
Pablo Matias Gomez
la source
78

Réponse 2017: vous pouvez désormais faire exactement ce que vous voulez dans chaque navigateur et nœud actuel

C'est assez simple:

  • Renvoyer une promesse
  • Utilisez l' attente , qui indiquera à JavaScript d'attendre que la promesse soit résolue en une valeur (comme la réponse HTTP)
  • Ajoutez le mot clé 'async' à la fonction parent

Voici une version de travail de votre code:

(async function(){

var response = await superagent.get('...')
console.log(response)

})()

attendre est pris en charge dans tous les navigateurs actuels et le nœud 8

mikemaccana
la source
7
Malheureusement, cela ne fonctionne qu'avec les fonctions qui renvoient des promesses - par exemple, cela ne fonctionne pas avec l'API Node.js, qui utilise des rappels. Et je ne recommanderais pas de l'utiliser sans Babel, car tout le monde n'utilise pas les "navigateurs actuels".
Michał Perłakowski
2
@ MichałPerłakowski node 8 inclut nodejs.org/api/util.html#util_util_promisify_original qui peut être utilisé pour faire les promesses de retour de l'API node.js. Le fait que vous ayez le temps et l'argent pour prendre en charge les navigateurs non actuels dépend évidemment de votre situation.
mikemaccana
IE 11 est toujours un navigateur actuel en 2018, malheureusement et il ne prend pas en chargeawait/async
Juan Mendes
IE11 n'est pas un navigateur actuel. Il a été publié il y a 5 ans, a une part de marché mondiale de 2,5% selon caniuse, et à moins que quelqu'un ne double votre budget pour ignorer toutes les technologies actuelles, cela ne vaut pas le temps de la plupart des gens.
mikemaccana
76

Js est un seul thread.

Le navigateur peut être divisé en trois parties:

1) Boucle d'événement

2) API Web

3) File d'attente des événements

La boucle d'événement fonctionne pour toujours, c'est-à-dire le type de boucle infinie. pour le suivant après que le premier soit exécuté. Cela signifie que l'exécution d'une fonction ne démarre pas tant que la fonction avant qu'elle ne soit en file d'attente ne soit exécutée dans la boucle d'événement.

Maintenant, pensons que nous avons poussé deux fonctions dans une file d'attente, l'une pour obtenir des données du serveur et une autre utilise ces données. Nous avons d'abord poussé la fonction serverRequest () dans la file d'attente puis la fonction utiliséData (). La fonction serverRequest va dans la boucle d'événements et fait un appel au serveur car nous ne savons jamais combien de temps il faudra pour obtenir des données du serveur, donc ce processus devrait prendre du temps et nous avons donc occupé notre boucle d'événements en suspendant ainsi notre page, c'est là que le Web L'API entre dans le rôle, elle prend cette fonction de la boucle d'événements et traite du fait que le serveur rend la boucle d'événements libre afin que nous puissions exécuter la fonction suivante de la file d'attente.La prochaine fonction dans la file d'attente est utiliseData () qui va en boucle mais en raison de l'absence de données disponibles, elle va le gaspillage et l'exécution de la fonction suivante se poursuivent jusqu'à la fin de la file d'attente (c'est ce qu'on appelle l'appel asynchrone, c'est-à-dire que nous pouvons faire autre chose jusqu'à ce que nous obtenions des données).

Supposons que notre fonction serverRequest () contienne une instruction return dans un code, lorsque nous récupérons les données du serveur, l'API Web les place dans la file d'attente à la fin de la file d'attente. Comme il est poussé à la fin de la file d'attente, nous ne pouvons pas utiliser ses données car il n'y a plus de fonction dans notre file d'attente pour utiliser ces données. Ainsi, il n'est pas possible de renvoyer quelque chose depuis un appel asynchrone.

Ainsi, la solution à cela est un rappel ou une promesse .

Une image d'une des réponses ici, explique correctement l'utilisation du rappel ... Nous donnons notre fonction (fonction utilisant les données renvoyées par le serveur) à la fonction serveur appelant.

Rappeler

 function doAjax(callbackFunc, method, url) {
  var xmlHttpReq = new XMLHttpRequest();
  xmlHttpReq.open(method, url);
  xmlHttpReq.onreadystatechange = function() {

      if (xmlHttpReq.readyState == 4 && xmlHttpReq.status == 200) {
        callbackFunc(xmlHttpReq.responseText);
      }


  }
  xmlHttpReq.send(null);

}

Dans mon code, il est appelé

function loadMyJson(categoryValue){
  if(categoryValue==="veg")
  doAjax(print,"GET","http://localhost:3004/vegetables");
  else if(categoryValue==="fruits")
  doAjax(print,"GET","http://localhost:3004/fruits");
  else 
  console.log("Data not found");
}

Rappel Javscript.info

Aniket Jha
la source
68

Vous pouvez utiliser cette bibliothèque personnalisée (écrite à l'aide de Promise) pour effectuer un appel à distance.

function $http(apiConfig) {
    return new Promise(function (resolve, reject) {
        var client = new XMLHttpRequest();
        client.open(apiConfig.method, apiConfig.url);
        client.send();
        client.onload = function () {
            if (this.status >= 200 && this.status < 300) {
                // Performs the function "resolve" when this.status is equal to 2xx.
                // Your logic here.
                resolve(this.response);
            }
            else {
                // Performs the function "reject" when this.status is different than 2xx.
                reject(this.statusText);
            }
        };
        client.onerror = function () {
            reject(this.statusText);
        };
    });
}

Exemple d'utilisation simple:

$http({
    method: 'get',
    url: 'google.com'
}).then(function(response) {
    console.log(response);
}, function(error) {
    console.log(error)
});
Vinoth Rajendran
la source
67

Une autre solution consiste à exécuter du code via l'exécuteur séquentiel nsynjs .

Si la fonction sous-jacente est promise

nsynjs évaluera toutes les promesses séquentiellement et mettra le résultat de la promesse dans la datapropriété:

function synchronousCode() {

    var getURL = function(url) {
        return window.fetch(url).data.text().data;
    };
    
    var url = 'https://ajax.googleapis.com/ajax/libs/jquery/2.0.0/jquery.min.js';
    console.log('received bytes:',getURL(url).length);
    
};

nsynjs.run(synchronousCode,{},function(){
    console.log('synchronousCode done');
});
<script src="https://rawgit.com/amaksr/nsynjs/master/nsynjs.js"></script>

Si la fonction sous-jacente n'est pas promise

Étape 1. Enveloppez la fonction avec rappel dans un wrapper prenant en charge nsynjs (s'il a une version promise, vous pouvez ignorer cette étape):

var ajaxGet = function (ctx,url) {
    var res = {};
    var ex;
    $.ajax(url)
    .done(function (data) {
        res.data = data;
    })
    .fail(function(e) {
        ex = e;
    })
    .always(function() {
        ctx.resume(ex);
    });
    return res;
};
ajaxGet.nsynjsHasCallback = true;

Étape 2. Mettez la logique synchrone en fonction:

function process() {
    console.log('got data:', ajaxGet(nsynjsCtx, "data/file1.json").data);
}

Étape 3. Exécutez la fonction de manière synchrone via nsynjs:

nsynjs.run(process,this,function () {
    console.log("synchronous function finished");
});

Nsynjs évaluera tous les opérateurs et expressions étape par étape, interrompant l'exécution au cas où le résultat d'une fonction lente n'est pas prêt.

Plus d'exemples ici: https://github.com/amaksr/nsynjs/tree/master/examples

amaksr
la source
2
C'est intéressant. J'aime la façon dont il permet de coder les appels asynchrones comme vous le feriez dans d'autres langues. Mais techniquement, ce n'est pas du vrai JavaScript?
J Morris
41

ECMAScript 6 dispose de «générateurs» qui vous permettent de programmer facilement dans un style asynchrone.

function* myGenerator() {
    const callback = yield;
    let [response] = yield $.ajax("https://stackoverflow.com", {complete: callback});
    console.log("response is:", response);

    // examples of other things you can do
    yield setTimeout(callback, 1000);
    console.log("it delayed for 1000ms");
    while (response.statusText === "error") {
        [response] = yield* anotherGenerator();
    }
}

Pour exécuter le code ci-dessus, procédez comme suit:

const gen = myGenerator(); // Create generator
gen.next(); // Start it
gen.next((...args) => gen.next([...args])); // Set its callback function

Si vous devez cibler des navigateurs qui ne prennent pas en charge ES6, vous pouvez exécuter le code via Babel ou le compilateur de fermeture pour générer ECMAScript 5.

Les rappels ...argssont enveloppés dans un tableau et déstructurés lorsque vous les lisez afin que le modèle puisse gérer les rappels qui ont plusieurs arguments. Par exemple avec le nœud fs :

const [err, data] = yield fs.readFile(filePath, "utf-8", callback);
James
la source
39

Voici quelques approches pour travailler avec des requêtes asynchrones:

  1. Objet Promise du navigateur
  2. Q - Une bibliothèque de promesses pour JavaScript
  3. A + Promises.js
  4. jQuery différé
  5. API XMLHttpRequest
  6. Utilisation du concept de rappel - Comme implémentation dans la première réponse

Exemple: implémentation différée jQuery pour travailler avec plusieurs requêtes

var App = App || {};

App = {
    getDataFromServer: function(){

      var self = this,
                 deferred = $.Deferred(),
                 requests = [];

      requests.push($.getJSON('request/ajax/url/1'));
      requests.push($.getJSON('request/ajax/url/2'));

      $.when.apply(jQuery, requests).done(function(xhrResponse) {
        return deferred.resolve(xhrResponse.result);
      });
      return deferred;
    },

    init: function(){

        this.getDataFromServer().done(_.bind(function(resp1, resp2) {

           // Do the operations which you wanted to do when you
           // get a response from Ajax, for example, log response.
        }, this));
    }
};
App.init();

Mohan Dere
la source
38

Nous nous trouvons dans un univers qui semble progresser selon une dimension que nous appelons «temps». Nous ne comprenons pas vraiment l'heure, mais nous avons développé des abstractions et un vocabulaire qui nous permettent de raisonner et d'en parler: "passé", "présent", "futur", "avant", "après".

Les systèmes informatiques que nous construisons - de plus en plus - ont le temps comme une dimension importante. Certaines choses devraient se produire à l'avenir. Ensuite, d'autres choses doivent se produire après que ces premières choses se sont finalement produites. C'est la notion de base appelée "asynchronicité". Dans notre monde de plus en plus en réseau, le cas le plus courant d'asynchronicité attend qu'un système distant réponde à une demande.

Prenons un exemple. Vous appelez le laitier et commandez du lait. Quand il vient, vous voulez le mettre dans votre café. Vous ne pouvez pas mettre le lait dans votre café en ce moment, car il n'est pas encore là. Vous devez attendre qu'il vienne avant de le mettre dans votre café. En d'autres termes, les éléments suivants ne fonctionneront pas:

var milk = order_milk();
put_in_coffee(milk);

Parce que JS n'a aucun moyen de savoir qu'il doit attendre pour order_milkterminer avant son exécution put_in_coffee. En d'autres termes, il ne sait pas que order_milkc'est asynchrone - c'est quelque chose qui ne produira pas de lait avant un certain temps. JS et d'autres langages déclaratifs exécutent une instruction après l'autre sans attendre.

L'approche JS classique de ce problème, tirant parti du fait que JS prend en charge les fonctions en tant qu'objets de première classe pouvant être transmis, consiste à transmettre une fonction en tant que paramètre à la demande asynchrone, qu'elle invoquera ensuite lorsqu'elle sera terminée. sa tâche dans le futur. C'est l'approche du "rappel". Cela ressemble à ceci:

order_milk(put_in_coffee);

order_milkdémarre, commande le lait, puis, quand et seulement quand il arrive, il invoque put_in_coffee.

Le problème avec cette approche de rappel est qu'elle pollue la sémantique normale d'une fonction rapportant son résultat avec return; à la place, les fonctions ne doivent pas rapporter leurs résultats en appelant un rappel donné en paramètre. En outre, cette approche peut rapidement devenir difficile à gérer lorsqu'il s'agit de séquences d'événements plus longues. Par exemple, disons que je veux attendre que le lait soit mis dans le café, puis ensuite seulement effectuer une troisième étape, à savoir boire le café. Je finis par avoir besoin d'écrire quelque chose comme ça:

order_milk(function(milk) { put_in_coffee(milk, drink_coffee); }

où je passe à la put_in_coffeefois au lait pour y mettre, et aussi à l'action ( drink_coffee) à exécuter une fois le lait introduit. Un tel code devient difficile à écrire, à lire et à déboguer.

Dans ce cas, nous pourrions réécrire le code de la question comme suit:

var answer;
$.ajax('/foo.json') . done(function(response) {
  callback(response.data);
});

function callback(data) {
  console.log(data);
}

Entrez des promesses

Telle était la motivation de la notion de «promesse», qui est un type particulier de valeur qui représente une sorte de résultat futur ou asynchrone . Cela peut représenter quelque chose qui s'est déjà produit, ou qui se produira à l'avenir, ou qui pourrait ne jamais se produire du tout. Les promesses ont une seule méthode, nommée then, à laquelle vous passez une action à exécuter lorsque le résultat que la promesse représente a été réalisé.

Dans le cas de notre lait et de notre café, nous prévoyons order_milkde retourner une promesse pour le lait arrivant, puis spécifions put_in_coffeecomme une thenaction, comme suit:

order_milk() . then(put_in_coffee)

Un avantage de ceci est que nous pouvons les enchaîner pour créer des séquences d'occurrences futures ("chaînage"):

order_milk() . then(put_in_coffee) . then(drink_coffee)

Appliquons des promesses à votre problème particulier. Nous allons envelopper notre logique de requête dans une fonction, qui retourne une promesse:

function get_data() {
  return $.ajax('/foo.json');
}

En fait, tout ce que nous avons fait est ajouté un returnà l'appel à $.ajax. Cela fonctionne parce que jQuery $.ajaxretourne déjà une sorte de chose semblable à une promesse. (En pratique, sans entrer dans les détails, nous préférerions encapsuler cet appel afin de renvoyer une vraie promesse, ou utiliser une alternative à $.ajaxcela.) Maintenant, si nous voulons charger le fichier et attendre qu'il se termine et alors faites quelque chose, on peut simplement dire

get_data() . then(do_something)

par exemple,

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

Lorsque nous utilisons des promesses, nous thenfinissons par transmettre de nombreuses fonctions , il est donc souvent utile d'utiliser les fonctions fléchées de style ES6 plus compactes:

get_data() . 
  then(data => console.log(data));

Le asyncmot clé

Mais il y a toujours quelque chose de vaguement insatisfaisant à devoir écrire du code dans un sens s'il est synchrone et d'une manière assez différente s'il est asynchrone. Pour synchrone, nous écrivons

a();
b();

mais s'il aest asynchrone, avec des promesses, nous devons écrire

a() . then(b);

Ci-dessus, nous avons dit: "JS n'a aucun moyen de savoir qu'il doit attendre la fin du premier appel avant d'exécuter le second". Ce ne serait pas bien s'il y avait un moyen de dire ça à JS? Il s'avère qu'il existe - le awaitmot - clé, utilisé à l'intérieur d'un type spécial de fonction appelé fonction "asynchrone". Cette fonctionnalité fait partie de la prochaine version d'ES mais est déjà disponible dans des transpilers tels que Babel, étant donné les bons préréglages. Cela nous permet d'écrire simplement

async function morning_routine() {
  var milk   = await order_milk();
  var coffee = await put_in_coffee(milk);
  await drink(coffee);
}

Dans votre cas, vous pourriez écrire quelque chose comme

async function foo() {
  data = await get_data();
  console.log(data);
}
utilisateur663031
la source
37

Réponse courte : votre foo()méthode renvoie immédiatement, tandis que l' $ajax()appel s'exécute de manière asynchrone après le retour de la fonction . Le problème est alors de savoir comment ou où stocker les résultats récupérés par l'appel asynchrone une fois qu'il revient.

Plusieurs solutions ont été données dans ce fil. Le moyen le plus simple consiste peut-être à passer un objet à la foo()méthode et à stocker les résultats dans un membre de cet objet une fois l'appel asynchrone terminé.

function foo(result) {
    $.ajax({
        url: '...',
        success: function(response) {
            result.response = response;   // Store the async result
        }
    });
}

var result = { response: null };   // Object to hold the async result
foo(result);                       // Returns before the async completes

Notez que l'appel à foo()ne retournera toujours rien d'utile. Cependant, le résultat de l'appel asynchrone sera désormais stocké dans result.response.

David R Tribble
la source
14
Bien que cela fonctionne, ce n'est pas vraiment mieux que d'affecter à une variable globale.
Felix Kling
36

Utilisez une callback()fonction à l'intérieur du foo()succès. Essayez de cette façon. C'est simple et facile à comprendre.  

var lat = "";
var lon = "";
function callback(data) {
    lat = data.lat;
    lon = data.lon;
}
function getLoc() {
    var url = "http://ip-api.com/json"
    $.getJSON(url, function(data) {
        callback(data);
    });
}

getLoc();
Mahfuzur Rahman
la source
29

La question était:

Comment renvoyer la réponse d'un appel asynchrone?

qui PEUT être interprété comme:

Comment rendre le code asynchrone synchrone ?

La solution consistera à éviter les rappels et à utiliser une combinaison de promesses et d' async / wait .

Je voudrais donner un exemple de demande Ajax.

(Bien qu'il puisse être écrit en Javascript, je préfère l'écrire en Python et le compiler en Javascript à l'aide de Transcrypt . Ce sera assez clair.)

Permet d'abord d'activer l'utilisation de JQuery, d'avoir $disponible en tant que S:

__pragma__ ('alias', 'S', '$')

Définissez une fonction qui renvoie une promesse , dans ce cas un appel Ajax:

def read(url: str):
    deferred = S.Deferred()
    S.ajax({'type': "POST", 'url': url, 'data': { },
        'success': lambda d: deferred.resolve(d),
        'error': lambda e: deferred.reject(e)
    })
    return deferred.promise()

Utilisez le code asynchrone comme s'il était synchrone :

async def readALot():
    try:
        result1 = await read("url_1")
        result2 = await read("url_2")
    except Exception:
        console.warn("Reading a lot failed")
Pieter Jan Bonestroo
la source
29

Utiliser Promise

La réponse la plus parfaite à cette question est d'utiliser Promise.

function ajax(method, url, params) {
  return new Promise(function(resolve, reject) {
    var xhr = new XMLHttpRequest();
    xhr.onload = function() {
      resolve(this.responseText);
    };
    xhr.onerror = reject;
    xhr.open(method, url);
    xhr.send(params);
  });
}

Usage

ajax("GET", "/test", "acrive=1").then(function(result) {
    // Code depending on result
})
.catch(function() {
    // An error occurred
});

Mais attendez...!

Il y a un problème avec l'utilisation des promesses!

Pourquoi devrions-nous utiliser notre propre promesse personnalisée?

J'utilisais cette solution depuis un certain temps jusqu'à ce que je comprenne qu'il y a une erreur dans les anciens navigateurs:

Uncaught ReferenceError: Promise is not defined

J'ai donc décidé d'implémenter ma propre classe Promise pour ES3 aux compilateurs js ci-dessous si elle n'est pas définie. Ajoutez simplement ce code avant votre code principal, puis utilisez Promise en toute sécurité!

if(typeof Promise === "undefined"){
    function _classCallCheck(instance, Constructor) {
        if (!(instance instanceof Constructor)) { 
            throw new TypeError("Cannot call a class as a function"); 
        }
    }
    var Promise = function () {
        function Promise(main) {
            var _this = this;
            _classCallCheck(this, Promise);
            this.value = undefined;
            this.callbacks = [];
            var resolve = function resolve(resolveValue) {
                _this.value = resolveValue;
                _this.triggerCallbacks();
            };
            var reject = function reject(rejectValue) {
                _this.value = rejectValue;
                _this.triggerCallbacks();
            };
            main(resolve, reject);
        }
        Promise.prototype.then = function then(cb) {
            var _this2 = this;
            var next = new Promise(function (resolve) {
                _this2.callbacks.push(function (x) {
                    return resolve(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.catch = function catch_(cb) {
            var _this2 = this;
            var next = new Promise(function (reject) {
                _this2.callbacks.push(function (x) {
                    return reject(cb(x));
                });
            });
            return next;
        };
        Promise.prototype.triggerCallbacks = function triggerCallbacks() {
            var _this3 = this;
            this.callbacks.forEach(function (cb) {
                cb(_this3.value);
            });
        };
        return Promise;
    }();
}
Amir Fo
la source
28

Bien sûr, il existe de nombreuses approches comme la demande synchrone, promis, mais d'après mon expérience, je pense que vous devriez utiliser l'approche de rappel. C'est naturel au comportement asynchrone de Javascript. Ainsi, votre extrait de code peut être réécrit un peu différemment:

function foo() {
    var result;

    $.ajax({
        url: '...',
        success: function(response) {
            myCallback(response);
        }
    });

    return result;
}

function myCallback(response) {
    // Does something.
}
Khoa Bui
la source
5
Il n'y a rien de intrinsèquement asynchrone dans les rappels ou JavaScript.
Aluan Haddad
19

Plutôt que de vous lancer du code, deux concepts sont essentiels pour comprendre comment JS gère les rappels et l'asynchronicité. (Est-ce que c'est un mot?)

Le modèle de boucle d'événement et de concurrence

Il y a trois choses dont vous devez être conscient; La queue; la boucle d'événements et la pile

En termes larges et simplistes, la boucle d'événement est comme le chef de projet, elle écoute constamment toutes les fonctions qui veulent s'exécuter et communique entre la file d'attente et la pile.

while (queue.waitForMessage()) {
   queue.processNextMessage();
}

Une fois qu'il reçoit un message pour exécuter quelque chose, il l'ajoute à la file d'attente. La file d'attente est la liste des choses en attente d'exécution (comme votre requête AJAX). imaginez-le comme ceci:

 1. call foo.com/api/bar using foobarFunc
 2. Go perform an infinite loop
 ... and so on

Lorsqu'un de ces messages va s'exécuter, il fait apparaître le message dans la file d'attente et crée une pile, la pile est tout ce dont JS a besoin pour exécuter l'instruction du message. Donc, dans notre exemple, on lui dit d'appelerfoobarFunc

function foobarFunc (var) {
  console.log(anotherFunction(var));
}

Donc, tout ce que foobarFunc doit exécuter (dans notre cas anotherFunction) sera poussé sur la pile. exécuté, puis oublié - la boucle d'événements se déplacera ensuite vers la prochaine chose dans la file d'attente (ou écoutera les messages)

L'élément clé ici est l'ordre d'exécution. C'est

QUAND est quelque chose qui va courir

Lorsque vous passez un appel à l'aide d'AJAX à un correspondant externe ou exécutez un code asynchrone (un setTimeout par exemple), Javascript dépend d'une réponse avant de pouvoir continuer.

La grande question est de savoir quand obtiendra-t-elle la réponse? La réponse est que nous ne savons pas - donc la boucle d'événements attend que ce message dise "hey run me". Si JS attendait ce message de manière synchrone, votre application se bloquerait et elle serait nulle. JS continue donc d'exécuter l'élément suivant dans la file d'attente en attendant que le message soit ajouté à la file d'attente.

C'est pourquoi, avec la fonctionnalité asynchrone, nous utilisons des choses appelées rappels . C'est un peu comme une promesse littéralement. Comme dans Je promets de retourner quelque chose à un moment donné, jQuery utilise des rappels spécifiques appelés deffered.done deffered.failet deffered.always(entre autres). Vous pouvez tous les voir ici

Donc, ce que vous devez faire, c'est passer une fonction qui est promise à exécuter à un moment donné avec des données qui lui sont transmises.

Parce qu'un rappel n'est pas exécuté immédiatement mais ultérieurement, il est important de transmettre la référence à la fonction non exécutée. donc

function foo(bla) {
  console.log(bla)
}

donc la plupart du temps (mais pas toujours) vous ne passerez foopasfoo()

J'espère que cela aura un certain sens. Lorsque vous rencontrez des choses comme celle-ci qui semblent déroutantes - je vous recommande fortement de lire la documentation entièrement pour au moins la comprendre. Cela fera de vous un bien meilleur développeur.

Matthew Brent
la source
18

En utilisant ES2017, vous devriez avoir ceci comme déclaration de fonction

async function foo() {
    var response = await $.ajax({url: '...'})
    return response;
}

Et l'exécuter comme ça.

(async function() {
    try {
        var result = await foo()
        console.log(result)
    } catch (e) {}
})()

Ou la syntaxe Promise

foo().then(response => {
    console.log(response)

}).catch(error => {
    console.log(error)

})
Fernando Carvajal
la source
cette deuxième fonction pourrait-elle être réutilisable ??
Zum Dummi
Comment utilisez-vous les résultats si oncolse, log est appelé? À ce stade, tout ne va-t-il pas simplement à la console?
Ken Ingram