→ 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, $.ajax
renvoie 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 success
rappel 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' findItem
exé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 avec
async/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 avec
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 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 async
et await
, vous pouvez écrire asynchrone dans un "style synchrone". Le code est toujours asynchrone, mais il est plus facile à lire / à comprendre.
async/await
s'appuie sur des promesses: une async
fonction 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' await
intérieur d'une async
fonction. Pour le moment, le niveau supérieur await
n'est pas encore pris en charge, vous devrez donc peut-être créer une async IIFE ( Immediateely Invoked Function Expression ) pour démarrer un async
contexte.
Vous pouvez en savoir plus sur async
et await
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);
})();
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 success
rappel. 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
});
}
callback
fera référence à la fonction à foo
laquelle 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 callback
et 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' if
instruction va toujours obtenir cet objet différé, le traiter comme true
et 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 XMLHTTPRequest
objet, passez false
comme troisième argument à .open
.
jQuery
Si vous utilisez jQuery , vous pouvez définir l' async
option sur false
. Notez que cette option est déconseillée depuis jQuery 1.8. Vous pouvez alors soit toujours utiliser un success
rappel, soit accéder à la responseText
proprié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).