Étant donné les exemples suivants, pourquoi outerScopeVar
est-il indéfini dans tous les cas?
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = 'lolcat.png';
alert(outerScopeVar);
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello Asynchronous World!';
}, 0);
alert(outerScopeVar);
// Example using some jQuery
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
});
alert(outerScopeVar);
// Node.js example
var outerScopeVar;
fs.readFile('./catdog.html', function(err, data) {
outerScopeVar = data;
});
console.log(outerScopeVar);
// with promises
var outerScopeVar;
myPromise.then(function (response) {
outerScopeVar = response;
});
console.log(outerScopeVar);
// geolocation API
var outerScopeVar;
navigator.geolocation.getCurrentPosition(function (pos) {
outerScopeVar = pos;
});
console.log(outerScopeVar);
Pourquoi sort-il undefined
dans tous ces exemples? Je ne veux pas de solutions de contournement, je veux savoir pourquoi cela se produit.
Note: Ceci est une question canonique pour JavaScript asynchronicité . N'hésitez pas à améliorer cette question et à ajouter d'autres exemples simplifiés auxquels la communauté peut s'identifier.
Une réponse d'un mot: asynchronicité .
Ce sujet a été itéré au moins quelques milliers de fois, ici, dans Stack Overflow. Par conséquent, je voudrais tout d’abord souligner quelques ressources extrêmement utiles:
Réponse de Felix Kling à "Comment renvoyer la réponse d'un appel asynchrone" . Voir son excellente réponse expliquant les flux synchrones et asynchrones, ainsi que la section "Code de restructuration".
@ Benjamin Gruenbaum a également déployé de nombreux efforts pour expliquer l’asynchronicité dans le même fil.
La réponse de Matt Esch à "Obtenir les données de fs.readFile" explique également très bien l'asynchronisme d'une manière simple.
Traçons d'abord le comportement courant. Dans tous les exemples, le outerScopeVar
est modifié à l'intérieur d'une fonction . Cette fonction n'est clairement pas exécutée immédiatement, elle est assignée ou passée en argument. C’est ce que nous appelons un rappel .
Maintenant la question est, quand ce rappel est appelé?
Cela dépend du cas. Essayons de tracer à nouveau un comportement courant:
img.onload
peut être appelé ultérieurement , lorsque (et si) l'image a été chargée avec succès.setTimeout
peut être appelé dans le futur , une fois le délai expiré et le délai écoulé non annulé par clearTimeout
. Remarque: même si vous utilisez 0
comme délai, tous les navigateurs ont un délai d'expiration minimal (défini à 4 ms dans la spécification HTML5).$.post
peut être appelé ultérieurement , lorsque (et si) la demande Ajax a été complétée avec succès.fs.readFile
de Node.js peut être appelé ultérieurement , lorsque le fichier a été lu avec succès ou qu'une erreur a été renvoyée.Dans tous les cas, nous avons un rappel qui peut exécuter ultérieurement . Ce "parfois dans le futur" est ce que nous appelons flux asynchrone .
L'exécution asynchrone est poussée hors du flux synchrone. C'est-à-dire que le code asynchrone ( ne s'exécutera jamais pendant l'exécution de la pile de code synchrone. Tel est le sens de JavaScript étant mono-threadé.
Plus précisément, lorsque le moteur JS est inactif (n'exécutant pas une pile de (a) code synchrone), il recherche les événements susceptibles d'avoir déclenché des rappels asynchrones (par exemple, un délai expiré, une réponse réseau reçue) et les exécute les uns après les autres. Ceci est considéré comme boucle d'événement .
C'est-à-dire que le code asynchrone mis en surbrillance dans les formes rouges dessinées à la main peut être exécuté uniquement après que tout le code synchrone restant dans leurs blocs de code respectifs a été exécuté:
En bref, les fonctions de rappel sont créées de manière synchrone mais exécutées de manière asynchrone. Vous ne pouvez tout simplement pas compter sur l'exécution d'une fonction asynchrone tant que vous ne saurez pas qu'elle s'est exécutée, et comment le faire?
C'est simple, vraiment. La logique qui dépend de l'exécution de la fonction asynchrone doit être démarrée/appelée depuis l'intérieur de cette fonction asynchrone. Par exemple, déplacer trop alert
s et console.log
s dans la fonction de rappel produirait le résultat attendu, car le résultat est disponible à ce stade.
Vous devez souvent faire plus avec le résultat d'une fonction asynchrone ou faire différentes choses avec le résultat en fonction du lieu où la fonction asynchrone a été appelée. Abordons un exemple un peu plus complexe:
var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);
function helloCatAsync() {
setTimeout(function() {
outerScopeVar = 'Nya';
}, Math.random() * 2000);
}
Remarque: J'utilise setTimeout
avec un retard aléatoire comme fonction asynchrone générique. Le même exemple s'applique à Ajax, readFile
, onload
et à tout autre flux asynchrone. .
Cet exemple présente clairement le même problème que les autres exemples: il n’attend pas que la fonction asynchrone soit exécutée.
Nous allons nous en occuper en mettant en place notre propre système de rappel. Tout d’abord, nous nous débarrassons de ce vilain outerScopeVar
qui est complètement inutile dans ce cas. Ensuite, nous ajoutons un paramètre qui accepte un argument de fonction, notre rappel. Lorsque l'opération asynchrone est terminée, nous appelons ce rappel en transmettant le résultat. La mise en œuvre (s'il vous plaît lire les commentaires dans l'ordre):
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
alert(result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
// 3. Start async operation:
setTimeout(function() {
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Extrait de code de l'exemple ci-dessus:
// 1. Call helloCatAsync passing a callback function,
// which will be called receiving the result from the async operation
console.log("1. function called...")
helloCatAsync(function(result) {
// 5. Received the result from the async function,
// now do whatever you want with it:
console.log("5. result is: ", result);
});
// 2. The "callback" parameter is a reference to the function which
// was passed as argument from the helloCatAsync call
function helloCatAsync(callback) {
console.log("2. callback here is the function passed as argument above...")
// 3. Start async operation:
setTimeout(function() {
console.log("3. start async operation...")
console.log("4. finished async operation, calling the callback, passing the result...")
// 4. Finished async operation,
// call the callback passing the result as argument
callback('Nya');
}, Math.random() * 2000);
}
Le plus souvent, dans des cas d'utilisation réels, l'API DOM et la plupart des bibliothèques fournissent déjà la fonctionnalité de rappel (l'implémentation helloCatAsync
dans cet exemple de démonstration). Il vous suffit de passer la fonction de rappel et de comprendre qu'elle s'exécutera hors du flux synchrone, et de restructurer votre code pour l'adapter à cela.
Vous remarquerez également qu'en raison de la nature asynchrone, il est impossible de return
une valeur d'un flux asynchrone au flux synchrone dans lequel le rappel a été défini, car les rappels asynchrones sont exécutés bien après l'exécution du code synchrone.
Au lieu de return
er une valeur provenant d'un rappel asynchrone, vous devrez utiliser le modèle de rappel, ou ... Promises.
Bien qu'il existe des moyens de garder callback enfer avec Vanilla JS, les promesses gagnent en popularité et sont en cours de normalisation dans ES6 (voir Promise - MDN ).
Promises (les contrats à terme) offrent une lecture plus linéaire, et donc agréable, du code asynchrone, mais expliquer toutes leurs fonctionnalités sort du cadre de cette question. Au lieu de cela, je laisserai ces excellentes ressources aux intéressés:
Remarque: J'ai marqué cette réponse comme Wiki de la communauté; toute personne ayant au moins 100 réputations peut l'éditer et l'améliorer! N'hésitez pas à améliorer cette réponse ou à soumettre une nouvelle réponse si vous le souhaitez.
Je veux transformer cette question en un sujet canonique pour répondre à des problèmes asynchrones qui ne sont pas liés à Ajax (il y a Comment renvoyer la réponse d'un appel AJAX?? ), d'où ce sujet a besoin de votre aide pour être aussi bon et utile que possible!
La réponse de Fabrício est sur place; mais je voulais compléter sa réponse par quelque chose de moins technique, qui repose sur une analogie pour aider à expliquer le concept d'asynchronicité .
Hier, le travail que je faisais demandait des informations à un collègue. Je lui ai téléphoné; voici comment s'est déroulée la conversation:
Moi : Bonjour Bob, j'ai besoin de savoir comment foo 'd the bar 'la semaine dernière. Jim veut un rapport à ce sujet et vous êtes le seul à en connaître les détails.
Bob : Bien sûr, mais ça me prendra environ 30 minutes?
Moi : C'est génial Bob. Donnez-moi une bague quand vous avez l'information!
À ce stade, j'ai raccroché le téléphone. Puisque j'avais besoin des informations de Bob pour compléter mon rapport, j'ai quitté le rapport et suis allé prendre un café à la place, puis j'ai rattrapé un courrier électronique. 40 minutes plus tard (Bob est lent), Bob a rappelé et m'a donné les informations dont j'avais besoin. À ce stade, j’ai repris mon travail avec mon rapport, car j’avais toutes les informations dont j’avais besoin.
Imaginez si la conversation s'était déroulée comme ceci à la place;
Moi : Bonjour Bob, j'ai besoin de savoir comment foo 'd the bar 'la semaine dernière. Jim veut un rapport à ce sujet et vous êtes le seul à en connaître les détails.
Bob : Bien sûr, mais ça me prendra environ 30 minutes?
Moi : C'est génial Bob. J'attendrai.
Et je me suis assis là et a attendu. Et attendu. Et attendu. Pendant 40 minutes. Ne rien faire d'autre que d'attendre. Finalement, Bob m'a donné l'information, nous avons raccroché et j'ai complété mon rapport. Mais j'avais perdu 40 minutes de productivité.
C'est exactement ce qui se passe dans tous les exemples de notre question. Charger une image, charger un fichier sur le disque et demander une page via AJAX sont toutes des opérations lentes (dans le contexte de l'informatique moderne).
Plutôt que d’attendre que ces opérations lentes soient terminées, JavaScript vous permet d’enregistrer une fonction de rappel qui sera exécutée une fois l’opération lente terminée. En attendant, JavaScript continuera à exécuter un autre code. Le fait que JavaScript exécute un autre code en attendant la fin de l'opération lente rend le comportement asynchrone . Si JavaScript avait attendu que l'opération se termine avant d'exécuter un autre code, il s'agirait d'un comportement synchrone .
var outerScopeVar;
var img = document.createElement('img');
// Here we register the callback function.
img.onload = function() {
// Code within this function will be executed once the image has loaded.
outerScopeVar = this.width;
};
// But, while the image is loading, JavaScript continues executing, and
// processes the following lines of JavaScript.
img.src = 'lolcat.png';
alert(outerScopeVar);
Dans le code ci-dessus, nous demandons à JavaScript de charger lolcat.png
, qui est une opération sloooow . La fonction de rappel sera exécutée une fois cette opération lente effectuée, mais entre-temps, JavaScript continuera à traiter les lignes de code suivantes. c'est-à-dire alert(outerScopeVar)
.
C'est pourquoi nous voyons l'alerte montrant undefined
; puisque la alert()
est traitée immédiatement, plutôt qu'après le chargement de l'image.
Afin de corriger notre code, tout ce que nous avons à faire est de déplacer le code alert(outerScopeVar)
dans la fonction de rappel. En conséquence, nous n’avons plus besoin de la variable outerScopeVar
déclarée comme variable globale.
var img = document.createElement('img');
img.onload = function() {
var localScopeVar = this.width;
alert(localScopeVar);
};
img.src = 'lolcat.png';
Vous allez toujours voir un rappel est spécifié en tant que fonction, car c'est le seul * moyen d'entrer JavaScript pour définir du code, mais ne l'exécutez que plus tard.
Par conséquent, dans tous nos exemples, la function() { /* Do something */ }
est le rappel; pour fixer tous les exemples, il suffit de déplacer le code qui nécessite la réponse de l'opération!
* Techniquement, vous pouvez aussi utiliser eval()
, mais eval()
est diabolique à cette fin.
Vous pourriez avoir actuellement un code similaire à celui-ci;
function getWidthOfImage(src) {
var outerScopeVar;
var img = document.createElement('img');
img.onload = function() {
outerScopeVar = this.width;
};
img.src = src;
return outerScopeVar;
}
var width = getWidthOfImage('lolcat.png');
alert(width);
Cependant, nous savons maintenant que le return outerScopeVar
arrive immédiatement; avant que la fonction de rappel onload
ait mis à jour la variable. Ceci conduit à getWidthOfImage()
renvoyer undefined
et à undefined
être alerté.
Pour résoudre ce problème, nous devons permettre à la fonction appelant getWidthOfImage()
d'enregistrer un rappel, puis de déplacer l'alerte de la largeur à l'intérieur de ce rappel;
function getWidthOfImage(src, cb) {
var img = document.createElement('img');
img.onload = function() {
cb(this.width);
};
img.src = src;
}
getWidthOfImage('lolcat.png', function (width) {
alert(width);
});
... comme précédemment, notez que nous avons pu supprimer les variables globales (dans ce cas, width
).
Voici une réponse plus concise pour ceux qui recherchent une référence rapide, ainsi que des exemples utilisant des promesses et async/wait.
Commencez par l'approche naïve (qui ne fonctionne pas) pour une fonction qui appelle une méthode asynchrone (dans ce cas, setTimeout
) et renvoie un message:
function getMessage() {
var outerScopeVar;
setTimeout(function() {
outerScopeVar = 'Hello asynchronous world!';
}, 0);
return outerScopeVar;
}
console.log(getMessage());
undefined
est enregistré dans ce cas parce que getMessage
est renvoyé avant que le rappel setTimeout
soit appelé et qu'il mette à jour outerScopeVar
.
Les deux manières principales de le résoudre utilisent des rappels et des promesses :
Rappels
Le changement ici est que getMessage
accepte un paramètre callback
qui sera appelé pour renvoyer les résultats au code appelant une fois disponible.
function getMessage(callback) {
setTimeout(function() {
callback('Hello asynchronous world!');
}, 0);
}
getMessage(function(message) {
console.log(message);
});
Les promesses offrent une alternative plus souple que les rappels car elles peuvent être naturellement combinées pour coordonner plusieurs opérations asynchrones. Une implémentation standard de Promises/A + est fournie nativement dans node.js (0.12+) et dans de nombreux navigateurs actuels, mais elle est également implémentée dans des bibliothèques telles que Bluebird et Q .
function getMessage() {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
getMessage().then(function(message) {
console.log(message);
});
jQuery Différé
jQuery fournit une fonctionnalité similaire aux promesses avec ses options différées.
function getMessage() {
var deferred = $.Deferred();
setTimeout(function() {
deferred.resolve('Hello asynchronous world!');
}, 0);
return deferred.promise();
}
getMessage().done(function(message) {
console.log(message);
});
async/wait
Si votre environnement JavaScript prend en charge async
et await
(comme Node.js 7.6+), vous pouvez utiliser les promesses de manière synchrone dans les fonctions async
:
function getMessage () {
return new Promise(function(resolve, reject) {
setTimeout(function() {
resolve('Hello asynchronous world!');
}, 0);
});
}
async function main() {
let message = await getMessage();
console.log(message);
}
main();
Les autres réponses sont excellentes et je souhaite simplement fournir une réponse directe à cette question. Simplement limiter aux appels asynchrones jQuery
Tous les appels ajax (y compris les $.get
ou $.post
ou $.ajax
) sont asynchrones.
Compte tenu de votre exemple
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2
outerScopeVar = response;
});
alert(outerScopeVar); //line 3
L’exécution du code commence à la ligne 1, déclare la variable, les déclencheurs et l’appel asynchrone de la ligne 2 (c’est-à-dire la demande de publication) et poursuit son exécution à partir de la ligne 3, sans attendre que la demande de publication ait terminé son exécution.
Disons que la demande de publication prend 10 secondes, la valeur de outerScopeVar
ne sera définie qu'après ces 10 secondes.
Essayer,
var outerScopeVar; //line 1
$.post('loldog', function(response) { //line 2, takes 10 seconds to complete
outerScopeVar = response;
});
alert("Lets wait for some time here! Waiting is fun"); //line 3
alert(outerScopeVar); //line 4
Lorsque vous exécutez cette opération, vous recevez une alerte sur la ligne 3. Maintenant, attendez un moment jusqu'à ce que vous soyez certain que la demande de publication a renvoyé une valeur. Ensuite, lorsque vous cliquez sur OK, dans la zone d'alerte, l'alerte suivante imprimera la valeur attendue, car vous l'avez attendue.
En situation réelle, le code devient,
var outerScopeVar;
$.post('loldog', function(response) {
outerScopeVar = response;
alert(outerScopeVar);
});
Tout le code qui dépend des appels asynchrones est déplacé à l'intérieur du bloc asynchrone ou en attente des appels asynchrones.
Dans tous ces scénarios, outerScopeVar
est modifié ou reçoit une valeur asynchrone ou survenant ultérieurement ( attente ou écoute d’un événement), pour lequel l’exécution en cours n’attendra pas . Ainsi, tous ces cas, le flux d’exécution en cours donne outerScopeVar = undefined
Discutons chaque exemple (j'ai marqué la partie appelée asynchrone ou différée pour que certains événements se produisent):
1.
Ici, nous enregistrons un eventlistner qui sera exécuté lors de cet événement particulier.Voici le chargement de l'image.Ensuite, l'exécution en cours continue avec les lignes suivantes img.src = 'lolcat.png';
et alert(outerScopeVar);
alors que l'événement ne peut pas se produire. c'est-à-dire que la fonction img.onload
attend le chargement de l'image référencée de manière asynchrone. Cela se produira tous les exemples suivants, l’événement peut différer.
2.
Ici, l'événement timeout joue le rôle, qui invoquera le gestionnaire après l'heure spécifiée. Ici, il s’agit de 0
, mais il enregistre néanmoins un événement asynchrone; il sera ajouté à la dernière position du Event Queue
pour l’exécution, ce qui crée le délai garanti.
3.
4.
Le nœud peut être considéré comme un roi du codage asynchrone. Ici, la fonction marquée est enregistrée en tant que gestionnaire de rappel qui sera exécuté après la lecture du fichier spécifié.
5.
Une promesse évidente (quelque chose sera fait à l'avenir) est asynchrone. voir Quelles sont les différences entre JavaScript, Deferred, Promise et Future?
https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript