Comment retourner une réponse d'un appel asynchrone?

J'ai une fonction foo qui fait une demande Ajax. Comment renvoyer la réponse de foo ?

J'ai essayé de renvoyer une valeur à partir du rappel de success et d'assigner la réponse à une variable locale de la fonction et de la renvoyer, mais aucune de ces méthodes ne renvoie de 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`. 
4687
08 янв. Felix Kling set Jan 08 2013-01-08 20:06 '13 à 20:06 2013-01-08 20:06
@ 39 réponses
  • 1
  • 2

→ Pour une explication plus générale du comportement asynchrone dans différents exemples, reportez-vous à la section Pourquoi ma variable ne change-t-elle pas après que je l'ai modifiée dans une fonction? - lien asynchrone vers le code

→ Si vous avez déjà compris le problème, reportez-vous aux solutions possibles ci-dessous.

Ce problème

A en Ajax signifie asynchrone . Cela signifie que l'envoi d'une demande (ou plutôt la réception d'une réponse) est supprimé du flux normal d'exécution. Dans votre exemple, $.ajax renvoyé immédiatement et le résultat suivant est return result; , est exécuté avant même que la fonction transmise comme rappel de success soit appelée.

Voici une analogie qui, espérons-le, clarifie la différence entre un flux synchrone et un flux asynchrone:

synchrone

Imaginez que vous appelez un ami et lui demandez de trouver quelque chose pour vous. Bien que cela puisse prendre un certain temps, vous attendez au téléphone et examinez l'espace jusqu'à ce que votre ami vous donne la réponse dont vous avez 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'exécution de findItem peut prendre du temps, le code suivant var item = findItem(); doit attendre que la fonction retourne un 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 aviez prévu. Dès que ton ami te rappellera, tu traiteras des informations qu'il t'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 une réponse, l'exécution continue immédiatement et l'instruction est exécutée après un appel Ajax. Pour finalement recevoir une réponse, vous fournissez une fonction qui sera appelée après réception de la réponse, un rappel (notez quelque chose, «call back»). Toute instruction suivant cet appel est exécutée avant l'appel de rappel.


Solution (s)

Adoptez la nature asynchrone de javascript! Bien que certaines opérations asynchrones fournissent des contreparties synchrones (comme "Ajax"), leur utilisation n’est généralement pas recommandée, en particulier dans le contexte du navigateur.

Pourquoi est-ce mauvais, demandez-vous?

JavaScript s'exécute dans le fil de l'interface utilisateur du navigateur et tout processus long bloque l'interface utilisateur, ce qui le rend inactif. En outre, il existe une limite supérieure d'exécution pour JavaScript et le navigateur demande à l'utilisateur s'il doit continuer ou non l'exécution.

Tout cela est vraiment une mauvaise expérience utilisateur. L'utilisateur ne pourra pas dire si tout fonctionne correctement ou non. En outre, l’effet sera pire pour les utilisateurs disposant d’une connexion lente.

Ensuite, nous examinons trois solutions différentes construites les unes sur les autres:

  • Promesses avec async/await wait (ES2017 +, disponible dans les navigateurs plus anciens si vous utilisez un transporteur ou un régénérateur)
  • Callbacks (populaires sur le site)
  • Promises with then() (ES2015 +, disponible dans les anciens navigateurs si vous utilisez l'une des nombreuses bibliothèques de promesses)

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


ES2017 +: promesses async/await attente

La version ECMAScript publiée en 2017 a introduit la prise en charge de la syntaxe pour les fonctions asynchrones. Avec async et await vous pouvez écrire de manière asynchrone dans un "style synchrone". Le code est toujours asynchrone, mais plus facile à lire / à comprendre.

async/await basé sur des promesses: la fonction async retourne toujours une promesse. await "déballe" la promesse et soit le résultat dans le sens de la promesse a été résolu avec, soit une erreur est générée si la promesse a été rejetée.

Important: vous ne pouvez utiliser await dans une fonction async . Actuellement, au plus haut niveau, await n’est pas encore pris en charge. Vous devrez peut-être obliger l’IseE asynchrone à démarrer le contexte async .

Vous pouvez en savoir plus sur async et await sur MDN.

Voici un exemple basé 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); })(); 

Les versions actuelles du navigateur et de l' hôte prennent en charge async/await . Vous pouvez également conserver des environnements plus anciens en convertissant votre code en ES5 à l'aide d'un régénérateur (ou d'outils utilisant un régénérateur, tel que Babel ).


Laissez les fonctions accepter les rappels.

Un rappel est simplement une fonction transmise à une autre fonction. Cette autre fonction peut appeler une fonction transmise dès qu’elle est prête. Dans le contexte d'un processus asynchrone, un rappel sera appelé chaque fois qu'un processus asynchrone est exécuté. Normalement, le résultat est envoyé au rappel.

Dans l'exemple de question, vous pouvez forcer foo à accepter un rappel et à l'utiliser comme rappel de success . Donc ça

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

devient

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

Ici nous définissons la fonction "inline", mais vous pouvez passer n'importe quelle référence à la 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 }); } 

callback fera référence à la fonction que nous passons à foo lorsque nous l'appelons, et nous la passons simplement au success . C'est à dire dès que la demande Ajax aboutit, $.ajax invoquera le callback et $.ajax réponse au rappel (qui peut être référencée à l'aide de result , puisque c'est ainsi que nous avons défini le rappel).

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

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

L'écriture de code à l'aide de callbacks est plus facile qu'il n'y paraît. Après tout, JavaScript dans le navigateur dépend fortement des événements (événements DOM). Obtenir une réponse Ajax n'est rien de plus 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 de l'application.


ES2015 +: promesses avec then ()

L'API Promise est une nouvelle fonctionnalité d'ECMAScript 6 (ES2015), mais elle est déjà prise en charge par le navigateur . De nombreuses bibliothèques implémentent également les promesses d’API standard et fournissent des méthodes supplémentaires pour simplifier l’utilisation et la composition de fonctions asynchrones (par exemple, bluebird ).

Les promesses sont des conteneurs pour les valeurs futures. Lorsqu'une promesse reçoit une valeur (elle est autorisée) ou lorsqu'elle est annulée (rejetée), elle en avertit tous ses "auditeurs" qui souhaitent accéder à cette valeur.

L'avantage par rapport aux rappels simples est qu'ils vous permettent de séparer votre code et qu'il est plus facile de les composer.

Voici un exemple simple d'utilisation des promesses:

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

En ce qui concerne notre appel à Ajax, nous pourrions utiliser les promesses suivantes:

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

Une description de tous les avantages qu'ils promettent d'offrir dépasse le cadre de cette réponse, mais si vous écrivez un nouveau code, vous devez sérieusement en tenir compte. Ils fournissent une excellente abstraction et séparation de votre code.

Plus d'infos sur les promesses: HTML5 - roches - Promesses JavaScript

Remarque: objets en attente jQuery

Les objets différés sont une implémentation personnalisée des promesses dans 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 jQuery Ajax renvoie déjà un "objet en attente" (en fait, une promesse d'un objet en attente), que vous pouvez simplement renvoyer à partir de sa fonction:

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

Note: la promesse s'est avérée

N'oubliez pas que les promesses et les objets différés ne sont que des conteneurs pour la valeur future, pas la valeur elle-même. Par exemple, supposons que vous ayez:

 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 asynchrones ci-dessus. En particulier, $.ajax() ne gèle pas le code lors de la vérification de la page '/ mot de passe' sur votre serveur. Il envoie une requête au serveur et, en attendant, renvoie immédiatement l'objet jQuery Ajax Deferred, et non la réponse du serveur. Cela signifie que l' if recevra toujours cet objet différé, le traitera comme true et agira comme si l'utilisateur était connecté. Pas bon

Mais corrigez-le facilement:

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

Non recommandé: appels synchrones "Ajax"

Comme je l'ai dit, certaines (!) Opérations asynchrones ont des contreparties synchrones. Je ne préconise pas leur utilisation, mais par souci d’exhaustivité, voici comment procéder pour effectuer un appel synchrone:

Pas de jQuery

Si vous utilisez directement l'objet XMLHTTPRequest , transmettez false tant que troisième argument .open .

Jquery

Si vous utilisez jQuery , vous pouvez définir le paramètre async sur false . Veuillez noter que cette option est obsolète depuis jQuery 1.8. Ensuite, vous pouvez toujours utiliser le rappel de success ou accéder à la propriété responseText de l'objet jqXHR :

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

Si vous utilisez une autre méthode Ajax jQuery, telle que $.get , $.getJSON , etc., vous devez la changer en $.ajax (car vous ne pouvez transmettre que les paramètres de configuration à $.ajax ).

Attention Impossible de faire une demande JSONP synchrone. JSONP est toujours de nature asynchrone (une autre raison de ne pas même envisager cette option).

5011
08 янв. La réponse est donnée par Felix Kling le 08 janvier 2013-01-08 20:06 '13 à 20:06 2013-01-08 20:06

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

Votre code devrait ressembler à 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 la réponse pour les personnes utilisant jQuery pour AJAX. J'ai décidé de proposer une alternative aux personnes qui n’en ont pas.

( Veuillez noter que pour ceux qui utilisent la nouvelle API, fetch , Angular ou promises, j'ai ajouté une autre réponse ci-dessous )


Ce que vous rencontrez

Ceci est un bref résumé de "l'explication du problème" tiré d'une autre réponse. Si vous n'êtes pas sûr, lisez ce qui suit.

A en AJAX signifie asynchrone . Cela signifie que l'envoi d'une demande (ou plutôt la réception d'une réponse) est supprimé du flux normal d'exécution. Dans votre exemple, .send renvoyé immédiatement et le résultat de l'instruction suivante est return result; est exécuté avant même que la fonction que vous avez transmise comme rappel de success n'ait été appelée.

Cela signifie que lorsque vous revenez, l'écouteur que vous avez spécifié n'est pas encore rempli, ce qui signifie que la valeur que vous avez renvoyée n'a pas été déterminée.

Analogie simple

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

(Feeddle)

La valeur de retour de a n'est undefined car la partie a=5 n'a pas encore été exécutée. AJAX effectue cette opération: vous renvoyez la valeur avant que le serveur ne puisse indiquer à votre navigateur quelle est la valeur.

Une solution possible à ce problème consiste à réutiliser le code, en informant votre programme de la procédure à suivre 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); } 

Ceci s'appelle CPS . En gros, nous getFive action à exécuter à la fin, nous expliquons à notre code comment réagir à la fin de l'événement (par exemple, notre appel AJAX ou, dans ce cas, un délai d'attente).

Utiliser:

 getFive(onComplete); 

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

Solutions possibles

border=0

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

  • Faites un appel AJAX synchrone (appelez-le SJAX).
  • Restructurez votre code pour qu'il fonctionne correctement avec les rappels.

1. AJAX synchrone - ne le faites pas !!

Quant à AJAX synchrone, ne le faites pas! La réponse de Felix donne de solides arguments pour expliquer pourquoi c’est une mauvaise idée. Pour résumer, cela gèlera le navigateur de l'utilisateur jusqu'à ce que le serveur renvoie une réponse et crée une très mauvaise interface utilisateur. Voici un autre résumé MDN expliquant pourquoi:

XMLHttpRequest prend en charge les communications synchrones et asynchrones. En général, cependant, les requêtes asynchrones devraient être préférables aux requêtes synchrones pour des raisons de performances.

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

Si vous avez besoin de faire cela, vous pouvez passer le 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 HTTP for 'ok' console.log(request.responseText); } 

2. Code de restructuration

Laissez votre fonction accepter le rappel. Dans l'exemple, le code foo peut être configuré pour accepter un rappel. Nous dirons à notre code comment réagir lorsque foo aura fini.

Donc:

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

devient:

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

Ici, nous avons passé une fonction anonyme, mais nous pourrions simplement passer un lien vers une fonction existante, le rendant ainsi:

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

Pour plus d'informations sur la façon dont ce type de rappel est exécuté, consultez la réponse Felix.

Définissons maintenant foo 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)

Maintenant, notre fonction foo accepte une action qui commence lorsque AJAX se termine avec succès, nous pouvons continuer en vérifiant si le statut de réponse 200 répond et agit en conséquence (création d'un gestionnaire d'erreurs, etc.). Une solution efficace à notre problème.

Si vous avez toujours du mal à comprendre cela, lisez le Guide de démarrage AJAX dans MDN.

953
30 мая '13 в 2:30 2013-05-30 02:30 Réponse donnée par Benjamin Gruenbaum le 30 mai '13 à 2:30 2013-05-30 02:30

XMLHttpRequest 2 (Lisez d'abord les réponses de Benjamin Grünbaum et Felix Kling )

Si vous n'utilisez pas jQuery et souhaitez obtenir un court XMLHttpRequest 2 qui fonctionne dans les navigateurs modernes, ainsi que dans les navigateurs mobiles, je suggère de l'utiliser comme suit:

 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 listées.
  2. Le rappel est établi directement (par conséquent, pas de fermetures inutiles inutiles).
  3. Je ne me souviens pas d’autres situations qui rendent XMLHttpRequest 1 ennuyeux.

Il existe deux manières d'obtenir une réponse à cet appel Ajax (trois à l'aide du nom de variable XMLHttpRequest):

Le plus simple:

 this.response 

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

 e.target.response 

Exemple:

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

Ou (les fonctions anonymes ci-dessus sont toujours un problème):

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

Il n'y a rien de plus facile.

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

Explorez les fonctionnalités avancées de XMLHttpRequest

Tous les navigateurs modernes sont supportés. Et je peux confirmer que j'utilise cette approche car il existe XMLHttpRequest 2. Je n'ai jamais rencontré de problèmes avec tous les navigateurs que j'utilise.

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

L'utilisation du nom de variable XMLHttpRequest est une autre grosse erreur, car vous devez rappeler à l'intérieur des 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 il reçoit et publie.

Exemples d'utilisation:

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

Ou passez l'é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 constater, je n'ai pas implémenté la synchronisation ... c'est mauvais.

Cela dit, pourquoi ne pas le faire simplement?


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

Gestionnaire d'erreur

 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 défini de manière statique afin qu'il ne compromette pas la fonction. Le gestionnaire d'erreurs peut être utilisé pour d'autres fonctions.

Mais pour vraiment obtenir l'erreur, le seul moyen est d'écrire la mauvaise URL, auquel cas chaque navigateur génère une erreur.