J'ai une fonction foo
qui fait une requête Ajax. Comment puis-je retourner la réponse de foo
?
J'ai essayé de renvoyer la valeur du callback success
ainsi que d'assigner la réponse à une variable locale à l'intérieur de la fonction et de renvoyer celle-ci, mais aucune de ces méthodes 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`.
&rarr ; Pour une explication plus générale du comportement asynchrone avec différents exemples, veuillez consulter https://stackoverflow.com/q/23667086/218196.
&rarr ; Si vous comprenez déjà le problème, passez aux solutions possibles ci-dessous.
Le problème
Le A de [Ajax][1] signifie [asynchrone][2] . 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,
$.ajax
retourne immédiatement et l'instruction suivante,return result;
, est exécutée avant même que la fonction que vous avez passée comme callbacksuccess
ait été appelée. Voici une analogie qui, nous l'espérons, rendra plus claire la différence entre flux synchrone et asynchrone :Synchrone
Imaginez que vous passez un coup de fil à un ami et que vous lui demandez de chercher quelque chose pour vous. Bien que cela puisse prendre un certain temps, vous attendez au téléphone, le regard dans le vide, jusqu'à ce que votre ami vous donne la réponse dont vous aviez besoin. La même chose se produit lorsque vous faites 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 findItem
peut prendre beaucoup de temps à s'exécuter, tout code venant après var item = findItem();
doit attendre que la fonction renvoie le résultat.
Vous appelez à nouveau votre ami pour la même raison. Mais cette fois, vous lui dites que vous êtes pressé et qu'il doit vous rappeler sur votre téléphone portable. Vous raccrochez, quittez la maison et faites ce que vous aviez prévu de faire. Une fois que votre ami vous rappelle, vous vous occupez des informations qu'il vous a données. C'est exactement ce qui se passe lorsque vous faites une requête Ajax.
findItem(function(item) {
// Do something with item
});
doSomethingElse();
**Bien que certaines opérations asynchrones offrent des contreparties synchrones (tout comme "Ajax"), il est généralement déconseillé de les utiliser, surtout dans le contexte d'un navigateur. Pourquoi est-ce mauvais, demandez-vous ? JavaScript s'exécute dans le fil d'exécution de l'interface utilisateur du navigateur et tout processus long bloque l'interface utilisateur, la rendant peu réactive. En outre, il existe une limite supérieure au temps d'exécution de JavaScript et le navigateur demande à l'utilisateur s'il souhaite poursuivre l'exécution ou non. Tout cela constitue une très mauvaise expérience pour l'utilisateur. L'utilisateur ne sera pas en mesure de dire si tout fonctionne bien ou non. En outre, l'effet sera pire pour les utilisateurs disposant d'une connexion lente. Dans les lignes qui suivent, nous allons examiner trois solutions différentes qui se superposent les unes aux autres :
async/await
(ES2017+, disponible dans les navigateurs plus anciens si vous utilisez un transpilateur ou un régénérateur).then()
(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 dans node 7+.**async/await
][3]La version d'ECMAScript publiée en 2017 a introduit un support de niveau syntaxique pour les fonctions asynchrones. Avec l'aide de async
et await
, vous pouvez écrire de l'asynchrone dans un "style synchrone". Le code est toujours asynchrone, mais il est plus facile à lire/comprendre.
async/await
s'appuie sur les promesses : une fonction async
renvoie toujours une promesse. await
"déballe" une promesse et renvoie soit la valeur avec laquelle la promesse a été résolue, soit lance une erreur si la promesse a été rejetée.
Important: Vous ne pouvez utiliser await
qu'à l'intérieur d'une fonction async
. Pour l'instant, le niveau supérieur de await
n'est pas encore supporté, donc vous devrez peut-être faire un IIFE asynchrone (Immediately Invoked Function Expression) pour démarrer un contexte async
.
Vous pouvez en savoir plus sur [async
][3] et [await
][4] sur 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);
})();
async/await
. Vous pouvez également prendre en charge des environnements plus anciens en transformant votre code en ES5 à l'aide de [regenerator][7] (ou d'outils qui utilisent regenerator, comme [Babel][8]).Un callback est simplement une fonction passée à une autre fonction. Cette autre fonction peut appeler la fonction passée dès qu'elle est prête. Dans le contexte d'un processus asynchrone, le callback sera appelé lorsque le processus asynchrone sera terminé. Habituellement, le résultat est transmis à la callback.
Dans l'exemple de la question, vous pouvez faire en sorte que foo
accepte un callback et l'utiliser comme callback success
. Donc, ceci
var result = foo();
// Code that depends on 'result'
devient
foo(function(result) {
// Code that depends on 'result'
});
Ici nous avons défini la fonction "inline" 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
});
}
callback
fera référence à la fonction que nous passons à foo
lorsque nous l'appelons et nous la passons simplement à success
. C'est-à-dire qu'une fois que la requête Ajax est réussie, $.ajax
appellera callback
et passera la réponse à la callback (qui peut être référencée avec result
, puisque c'est ainsi que nous avons défini la callback).
Vous pouvez également traiter la réponse avant de la passer au callback :
function foo(callback) {
$.ajax({
// ...
success: function(response) {
// For example, filter the response
callback(filtered_response);
}
});
}
L'API [Promise][9] est une nouvelle fonctionnalité de l'ECMAScript 6 (ES2015), mais elle bénéficie déjà d'un bon [support des navigateurs][10]. Il existe également de nombreuses bibliothèques qui mettent en œuvre l'API Promises standard et fournissent des méthodes supplémentaires pour faciliter l'utilisation et la composition de fonctions asynchrones (par exemple, [bluebird][11]). Les promesses sont des conteneurs pour des valeurs futures. Lorsque la promesse reçoit la valeur (elle est resolved) ou lorsqu'elle est annulée (rejected), elle notifie tous ses " listeners " qui veulent accéder à cette valeur. L'avantage par rapport aux callbacks classiques 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 ceci :
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
});
La description de tous les avantages offerts par les promesses dépasse le cadre de cette réponse, mais si vous écrivez du nouveau code, vous devriez sérieusement les envisager. Elles fournissent une grande abstraction et séparation de votre code. Plus d'informations sur les promesses : [HTML5 rocks - JavaScript Promises] [12]
[Les objets différés][13] sont l'implémentation personnalisée des promesses par jQuery (avant la normalisation de l'API Promise). Ils se comportent presque comme les promesses mais exposent une API légèrement différente. Chaque méthode Ajax de jQuery renvoie déjà un "objet différé" (en fait une promesse d'un objet différé) que vous pouvez simplement renvoyer depuis votre fonction :
function ajax() {
return $.ajax(...);
}
ajax().done(function(result) {
// Code depending on result
}).fail(function() {
// An error occurred
});
Gardez à l'esprit que les promesses et les objets différés sont juste des contenants pour une valeur future, ils ne sont pas la valeur elle-même. Par exemple, supposons que vous ayez ce qui suit :
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 ne comprend pas les problèmes d'asynchronie mentionnés ci-dessus. Plus précisément, $.ajax()
ne fige pas le code pendant qu'il vérifie la page '/password' sur votre serveur - il envoie une requête au serveur et pendant qu'il attend, renvoie immédiatement un objet jQuery Ajax Deferred, et non la réponse du serveur. Cela signifie que l'instruction if
va toujours récupérer cet objet différé, le traiter comme true
, et procéder comme si l'utilisateur était connecté. Ce n'est 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
});
Comme je l'ai mentionné, certaines( !) opérations asynchrones ont des équivalents synchrones. Je ne préconise pas leur utilisation, mais par souci d'exhaustivité, voici comment effectuer un appel synchrone :
Si vous utilisez directement un objet [XMLHTTPRequest
][14], passez false
comme troisième argument à [.open
][15].
Si vous utilisez [jQuery][16], vous pouvez mettre l'option async
à false
. Notez que cette option est dépréciée depuis jQuery 1.8.
Vous pouvez alors soit utiliser un callback success
, soit accéder à la propriété responseText
de l'objet [jqXHR][17] :
function foo() {
var jqXHR = $.ajax({
//...
async: false
});
return jqXHR.responseText;
}
Si vous utilisez une autre méthode Ajax de jQuery, comme $.get
, $.getJSON
, etc., vous devez la changer en $.ajax
(puisque vous ne pouvez passer que des paramètres de configuration à $.ajax
).
**Il n'est pas possible d'effectuer une requête [JSONP][18] synchrone. Par sa nature même, JSONP est toujours asynchrone (une raison de plus pour ne pas envisager cette option).
[1] : https://en.wikipedia.org/wiki/Ajax_(programmation)
[2] : https://www.merriam-webster.com/dictionary/asynchronous
[3] : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/async_function
[4] : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/await
[5] : https://kangax.github.io/compat-table/es2016plus/#test-async_functions
[6] : http://node.green/#ES2017-features-async-functions
[7] : https://github.com/facebook/regenerator
[8] : https://babeljs.io/
[9] : https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[10] : http://caniuse.com/#feat=promises "caniuse"
[11] : https://github.com/petkaantonov/bluebird
[12] : http://www.html5rocks.com/en/tutorials/es6/promises/
[13] : https://stackoverflow.com/questions/4866721/what-are-deferred-objects
[14] : http://www.w3.org/TR/XMLHttpRequest/
[15] : http://www.w3.org/TR/XMLHttpRequest/#the-open%28%29-method
[16] : http://api.jquery.com/jQuery.ajax/
[17] : http://api.jquery.com/jQuery.ajax/#jqXHR
[18] : https://stackoverflow.com/questions/2067472/please-explain-jsonp
Votre code devrait ressembler à quelque chose comme 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 font pas.
Ceci est un court résumé de "l'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 requête (ou plutôt la réception de la réponse) est retiré du flux d'exécution normal. Dans votre exemple, [.send
][1] retourne immédiatement et l'instruction suivante, return result;
, est exécutée avant même que la fonction que vous avez passée comme callback success
ait été appelée.
Cela signifie que lorsque vous retournez, l'écouteur que vous avez défini n'a pas encore été 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;
}
[(Fiddle)][2]
La valeur de a
retournée est undefined
puisque la partie a=5
n'a pas encore été exécutée. AJAX agit de la sorte, vous renvoyez la valeur avant que le serveur n'ait eu la possibilité de dire à votre navigateur quelle est cette valeur.
Une solution possible à ce problème est de coder de manière "réactive", en indiquant à votre programme ce qu'il doit faire lorsque le calcul est 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 le [CPS][3]. En fait, nous passons à getFive
une action à exécuter lorsqu'elle se termine, nous disons à notre code comment réagir lorsqu'un événement se termine (comme notre appel AJAX, ou dans ce cas le délai d'attente).
L'utilisation serait :
getFive(onComplete);
Ce qui devrait alerter "5" à l'écran. [(Fiddle)][4].
Il y a essentiellement deux façons de résoudre ce problème :
En ce qui concerne AJAX synchrone, ne le faites pas! La réponse de Felix soulève quelques arguments convaincants sur les raisons pour lesquelles c'est une mauvaise idée. Pour résumer, cela va geler le navigateur de l'utilisateur jusqu'à ce que le serveur renvoie la réponse et créer une très mauvaise expérience utilisateur. Voici un autre court résumé tiré de MDN sur les raisons de cette pratique :
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... ...ce qui peut causer de sérieux problèmes...
Si vous vous devez le faire, vous pouvez passer un drapeau : [Voici comment :] [5]
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);
}
Laissez votre fonction accepter un callback. Dans l'exemple de code, on peut faire en sorte que foo
accepte un callback. Nous allons dire à notre code comment réagir lorsque foo
se termine.
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 tout aussi bien passer une référence à une fonction existante, ce qui donnerait l'exemple suivant :
function myHandler(result) {
// code that depends on `result`
}
foo(myHandler);
Pour plus de détails sur la conception de ce type de callback, consultez la réponse de Felix.
Maintenant, définissons foo lui-même pour qu'il agisse 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();
}
[(fiddle)][6]
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 étendre cela en vérifiant si le statut de la réponse n'est pas 200 et agir en conséquence (créer un gestionnaire d'échec et autres). Ce qui résout efficacement notre problème.
Si vous avez encore du mal à comprendre, lisez le guide de démarrage d'AJAX [7] sur MDN.
[1] : https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest#send%28%29 [2] :
[3] : http://en.wikipedia.org/wiki/Continuation-passing_style [4] : http://jsfiddle.net/PAjZR/ [5] : https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Synchronous_and_Asynchronous_Requests#Synchronous_request [6] : http://jsfiddle.net/DAcWT/ [7] : https://developer.mozilla.org/en-US/docs/AJAX/Getting_Started[XMLHttpRequest][1] 2 (lisez d'abord les réponses de Benjamin Gruenbaum & Felix Kling) Si vous n'utilisez pas jQuery et que vous voulez un XMLHttpRequest 2 court et agréable qui fonctionne sur les navigateurs modernes et également sur les navigateurs mobiles, je vous 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 :
this.response
Ou si pour une raison quelconque vous bind()
le callback à une classe :
e.target.response
Exemple :
function callback(e){
console.log(this.response);
}
ajax('URL', callback);
Ou (le précédent est meilleur les fonctions anonymes sont toujours un problème) :
ajax('URL', function(e){console.log(this.response)});
XMLHttpRequest
est une autre grosse erreur car vous devez exécuter le callback à l'intérieur des fermetures onload/oreadystatechange sinon vous le perdez.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 permet d'obtenir & post. 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éfinir des valeurs personnalisées :
var fd = new FormData();
fd.append('key', 'val')
x(url, callback, 'post', fd);
Comme mentionné dans le commentaire, l'utilisation de error && synchronous casse complètement le point de la réponse. Quel est le moyen le plus court d'utiliser Ajax de manière appropriée ? *Manipulateur 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);
displayAjax()
sous this.statusText
comme Method not Allowed
.
Dans le second cas, cela fonctionne tout simplement. Vous devez vérifier du côté du serveur si vous avez passé les bonnes données de post.
cross-domain not allowed lance automatiquement une erreur.
Dans la réponse d'erreur, il n'y a pas de code d'erreur.
Il y a seulement le this.type
qui est mis à error.
Pourquoi ajouter un gestionnaire d'erreur si vous n'avez absolument aucun contrôle sur les erreurs ?
La plupart des erreurs sont renvoyées dans la fonction de rappel displayAjax()
.
Donc : Pas besoin de contrôle d'erreur si vous êtes capable de copier et coller l'URL correctement ;)
PS : Pour le premier test, j'ai écrit x('x', displayAjax)..., et j'ai eu une réponse... ???? J'ai donc vérifié le dossier où se trouve le 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 bien rigoléLire un fichier de manière synchrone
Ne faites pas ça.
Si vous voulez bloquer le navigateur pendant un moment, chargez un gros fichier .txt
de manière 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');
Les fonctions ci-dessus sont pour une utilisation de base. Si vous voulez EXTENSIONNER la fonction... Oui, vous le pouvez. J'utilise beaucoup d'APIs et l'une des premières fonctions que j'intègre dans chaque page HTML est la première fonction Ajax de 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 (utilisant des plages des deux côtés avec resume, filereader, filesystem), divers convertisseurs de redimensionnement d'images utilisant canvas, peupler des bases de données web SQL avec base64images et bien plus encore... Mais dans ces cas, vous devriez créer une fonction uniquement dans ce but... parfois vous avez besoin d'un blob, d'un tableau de tampons, vous pouvez définir des en-têtes, remplacer le mimetype et il y a beaucoup plus... Mais la question ici est de savoir comment retourner une réponse Ajax... (J'ai ajouté un moyen facile). [1] : http://en.wikipedia.org/wiki/XMLHttpRequest
[4] : http://caniuse.com/xhr2