Pourquoi ma variable est-elle inchangée après ma modification dans une fonction? - Référence de code asynchrone

669

Étant donné les exemples suivants, pourquoi est outerScopeVarindé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 apparaît-il undefineddans tous ces exemples? Je ne veux pas de solutions de contournement, je veux savoir pourquoi cela se produit.


Remarque: 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.

Fabrício Matté
la source
@Dukeling merci, je suis à peu près sûr que j'ai commenté avec ce lien, mais il manque apparemment des commentaires. De plus, en ce qui concerne votre montage: je pense qu'avoir "canonique" et "asynchronicité" dans le titre aide à rechercher cette question pour marquer une autre question comme dupe. Et bien sûr, cela aide également à trouver cette question de Google lors de la recherche d'explications asynchrones.
Fabrício Matté
3
En réfléchissant un peu plus, "le sujet canonique de l'asynchronicité" est un peu lourd sur le titre, "la référence de code asynchrone" est plus simple et plus objectif. Je crois aussi que la plupart des gens recherchent "asynchrone" au lieu de "asynchronicité".
Fabrício Matté
1
Certaines personnes initialisent leur variable avant l'appel de la fonction. Pourquoi ne pas changer le titre qui le représente aussi? Comme "Pourquoi ma variable est-elle inchangée après que je l'ai modifiée à l'intérieur d'une fonction?" ?
Felix Kling
Dans tous les exemples de code que vous avez mentionnés ci-dessus, "alert (outerScopeVar);" exécute NOW, alors que l'attribution de valeur à "outerScopeVar" se produit PLUS TARD (de manière asynchrone).
refactor

Réponses:

542

Réponse en un mot: asynchronicité .

Les mots

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:


La réponse à la question posée

Traçons d'abord le comportement courant. Dans tous les exemples, le outerScopeVarest 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.onloadpeut être appelé ultérieurement , lorsque (et si) l’image a été chargée avec succès.
  • setTimeoutpeut être appelé ultérieurement , une fois le délai expiré et le délai d’expiration écoulé clearTimeout. Remarque: même en cas d'utilisation en 0tant que délai, tous les navigateurs ont une limite de délai d'expiration minimale (spécifiée à 4 ms dans la spécification HTML5).
  • $.postLe rappel de jQuery peut être appelé ultérieurement , lorsque (et si) la demande Ajax a été complétée avec succès.
  • Node.js fs.readFilepeut ê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 être exécuté ultérieurement . Ce "dans le futur" est ce que nous appelons un flux asynchrone .

L'exécution asynchrone est poussée hors du flux synchrone. C'est-à-dire que le code asynchrone ne sera jamais exécuté pendant l'exécution de la pile de code synchrone. Tel est le sens de JavaScript étant mono-threadé.

Plus spécifiquement, 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 une 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é:

code async mis en évidence

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, si vous déplacez trop alerts et console.logs à l'intérieur de la fonction de rappel, le résultat attendu est généré, car le résultat est disponible à ce stade.

Implémentation de votre propre logique de rappel

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

Note: J'utilise setTimeoutavec un retard aléatoire en fonction asynchrone générique, le même exemple s'applique à Ajax, readFile, onloadet 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 outerScopeVarqui 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' helloCatAsyncimplémentation 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 returnrenvoyer 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 d' returnutiliser une valeur provenant d'un rappel asynchrone, vous devrez utiliser le modèle de rappel ou ... les promesses.

Promesses

Même s’il existe des moyens de garder l’ enfer des rappels avec JS vanilla, les promesses gagnent en popularité et sont en cours de normalisation dans ES6 (voir Promise - MDN ).

Promises (aka Futures) fournit une lecture plus linéaire, et donc agréable, du code asynchrone, mais l'explication de l'ensemble de leurs fonctionnalités dépasse le cadre de cette question. Au lieu de cela, je laisserai ces excellentes ressources aux intéressés:


Plus de matériel de lecture sur l'asynchronisme JavaScript


Remarque: j'ai marqué cette réponse comme étant un wiki de communauté. Toute personne disposant d'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 souhaite faire de cette question un sujet canonique pour répondre à des problèmes d'asynchronicité non liés à Ajax (il y a comment renvoyer la réponse d'un appel AJAX? Pour cela), c'est pourquoi ce sujet a besoin de votre aide pour être aussi efficace et utile que possible. !

Fabrício Matté
la source
1
Dans votre dernier exemple, y a-t-il une raison spécifique pour laquelle vous utilisez des fonctions anonymes ou fonctionneriez-vous de la même manière en utilisant des fonctions nommées?
JDelage
1
Les exemples de code sont un peu bizarres car vous déclarez la fonction après l'avoir appelée. Fonctionne à cause du levage bien sûr, mais était-ce intentionnel?
Bergi
2
est-ce une impasse? felix kling pointe vers votre réponse et vous pointez sur felix answer
Mahi
1
Vous devez comprendre que le code du cercle rouge n’est asynchrone que parce qu’il est exécuté par les fonctions javascript async NATIVE. Il s’agit d’une fonctionnalité de votre moteur javascript, qu’il s’agisse de Node.js ou d’un navigateur. Il est asynchrone car il est transmis en tant que "rappel" à une fonction qui est essentiellement une boîte noire (implémentée en C, etc.). Pour le développeur malheureux, ils sont async ... juste parce que. Si vous voulez écrire votre propre fonction asynchrone, vous devez la pirater en l'envoyant à SetTimeout (myfunc, 0). Devriez-vous faire ça? Un autre débat .... probablement pas.
Sean Anderson
@Fabricio J'ai cherché la spécification définissant le "> = 4ms clamp", mais je ne l'ai pas trouvée. J'ai trouvé quelques mentions d'un mécanisme similaire (pour le blocage d'appels imbriqués) sur MDN - developer.mozilla.org/en-US/docs / Web / API /… - Quelqu'un a-t-il un lien vers la partie droite de la spécification HTML?
Sebi
147

La réponse de Fabrício est parfaite. 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é .


Une analogie ...

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 : Salut Bob, j'ai besoin de savoir comment nous avons foutu le 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 : Salut Bob, j'ai besoin de savoir comment nous avons foutu le 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 un comportement asynchrone ou synchrone

C'est exactement ce qui se passe dans tous les exemples de notre question. Charger une image, charger un fichier sur un disque et demander une page via AJAX sont des opérations lentes (dans le contexte de l'informatique moderne).

Plutôt que d’ attendre la fin de ces opérations lentes, 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 se charger lolcat.png, opération qui est une opération lente . 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 le alert()est traité 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 alert(outerScopeVar)code dans la fonction de rappel. En conséquence, nous n’avons plus besoin de la outerScopeVarvariable 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 verrez toujours qu'un rappel est spécifié en tant que fonction, car c'est la seule façon * de JavaScript de définir du code, mais de ne l'exécuter que plus tard.

Par conséquent, dans tous nos exemples, le function() { /* Do something */ }est le rappel; pour corriger tous les exemples, tout ce que nous avons à faire est de déplacer le code qui nécessite la réponse de l'opération!

* Techniquement, vous pouvez eval()aussi l' utiliser , mais eval()c'est mal pour ce but


Comment puis-je garder mon correspondant en attente?

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 cela return outerScopeVarse produit immédiatement; avant que la onloadfonction de rappel ait mis à jour la variable. Cela conduit à getWidthOfImage()revenir undefinedet à undefinedêtre alerté.

Pour résoudre ce problème, nous devons permettre à l'appelant getWidthOfImage()de la fonction 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).

Matt
la source
Mais en quoi l’alerte ou l’envoi à la console est-il utile si vous souhaitez utiliser les résultats dans un calcul différent ou les stocker dans une variable d’objet?
Ken Ingram
68

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

undefinedest enregistré dans ce cas car getMessageretourne avant que le setTimeoutrappel soit appelé et se met à jour outerScopeVar.

Les deux principaux moyens de le résoudre utilisent des rappels et des promesses :

Rappels

La modification proposée ici consiste à getMessageaccepter un callbackparamètre 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);
});

Promesses

Les promesses offrent une alternative plus souple que les rappels car elles peuvent être naturellement combinées pour coordonner plusieurs opérations asynchrones. A Promises / A + de la mise en œuvre standard est nativement prévu dans Node.js (0.12+) et de nombreux navigateurs actuels, mais il est également mis en œuvre dans les bibliothèques comme 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);  
});

asynchrone / wait

Si votre environnement JavaScript prend en charge asyncet await(comme Node.js 7.6+), vous pouvez utiliser les promesses de manière synchrone dans les asyncfonctions suivantes:

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();
JohnnyHK
la source
Votre échantillon sur Promises est ce que je recherche depuis quelques heures. Votre exemple est magnifique et explique Promises en même temps. Pourquoi ce n’est nulle part ailleurs est ahurissant.
Vincent P
Tout va bien, mais que se passe-t-il si vous devez appeler getMessage () avec des paramètres? Comment écririez-vous ce qui précède dans ce scénario?
Chiwda
2
@Chiwda Vous mettez juste le paramètre de rappel dernier: function getMessage(param1, param2, callback) {...}.
JohnnyHK
J'essaie votre async/awaitéchantillon, mais je rencontre des problèmes. Au lieu d'instancier un new Promise, je passe un .Get()appel et n'ai donc accès à aucune resolve()méthode. Ainsi, ma getMessage()retourne la promesse et non le résultat. Pourriez-vous modifier un peu votre réponse pour montrer une syntaxe de travail pour cela?
InteXX
@InteXX Je ne sais pas trop ce que vous entendez par un .Get()appel. Le mieux est probablement de poster une nouvelle question.
JohnnyHK
52

Pour énoncer l'évidence, la coupe représente outerScopeVar.

Fonctions asynchrones être comme ...

appel asynchrone pour le café

Johannes Fahrenkrug
la source
13
Tandis que tenter de faire fonctionner une fonction asynchrone de manière synchrone, ce serait essayer de boire le café en 1 seconde et de le verser sur vos genoux en 1 minute.
Teepeemm
Si elle énonçait une évidence, je ne pense pas que la question aurait été posée, non?
broccoli2000
2
@ broccoli2000 Par là, je ne voulais pas dire que la question était évidente, mais qu'il est évident ce que la coupe représente dans le dessin :)
Johannes Fahrenkrug
13

Les autres réponses sont excellentes et je souhaite simplement donner une réponse directe à cette question. Simplement limiter aux appels asynchrones jQuery

Tous les appels ajax ( $.getou $.postou $.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 sur la ligne 2 (c.-à-d. 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 outerScopeVarne 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.

Dans un scénario réel, 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.

Teja
la source
or by waiting on the asynchronous callsComment on fait ça?
InteXX
@InteXX En utilisant une méthode de rappel
Teja
Avez-vous un exemple de syntaxe rapide?
InteXX le
10

Dans tous ces scénarios, outerScopeVarune valeur est modifiée ou affectée de manière asynchrone ou se produit ultérieurement (en attente ou en écoute pour qu'un événement se produise), pour lequel l'exécution actuelle n'attendra pas .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.

entrez la description de l'image ici

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);entre-temps, l'événement peut ne pas se produire. c'est-à-dire que la fonction img.onloadattend que l'image référencée se charge, de manière asynchrone. Cela se produira tous les exemples suivants, l’événement peut différer.

2

2

Ici, l'événement timeout joue le rôle, qui invoquera le gestionnaire après l'heure spécifiée. Le voici 0, mais il enregistre quand même un événement asynchrone. Il sera ajouté à la dernière position du Event Queuefor pour l'exécution, ce qui crée le délai garanti.

3

entrez la description de l'image ici Cette fois, le rappel ajax.

4

entrez la description de l'image ici

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

entrez la description de l'image ici

Une promesse évidente (quelque chose sera fait à l'avenir) est asynchrone. voir Quelles sont les différences entre JavaScript, les droits différés, Promise et Future?

https://www.quora.com/Whats-the-difference-between-a-promise-and-a-callback-in-Javascript

Tom Sebastian
la source