Pourquoi ma variable est-elle inchangée après l'avoir modifiée à l'intérieur d'une fonction? - Référence de code asynchrone

May 15 2014

Compte tenu des exemples suivants, pourquoi est-il 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: Il s'agit d'une question canonique pour l' asynchronisme JavaScript . N'hésitez pas à améliorer cette question et à ajouter des exemples plus simplifiés auxquels la communauté peut s'identifier.

Réponses

605 FabrícioMatté May 15 2014 at 06:55

Réponse en un mot: asynchronicité .

Préface

Ce sujet a été répété au moins quelques milliers de fois, ici, dans Stack Overflow. Par conséquent, tout d'abord, j'aimerais souligner quelques ressources extrêmement utiles:


La réponse à la question posée

Trouvons d'abord le comportement commun. 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 de savoir quand ce rappel est-il appelé?

Cela dépend du cas. Essayons à nouveau de retracer un comportement courant:

  • img.onloadpeut être appelée dans le futur , quand (et si) l'image a été chargée avec succès.
  • setTimeoutpeut être appelé dans le futur , après l'expiration du délai et le délai d'attente n'a pas été annulé par clearTimeout. Remarque: même lorsqu'ils sont utilisés 0comme délai, tous les navigateurs ont un délai d'expiration minimal (spécifié à 4 ms dans la spécification HTML5).
  • $.postLe rappel de jQuery peut être appelé dans le futur , quand (et si) la requête Ajax a été exécutée avec succès.
  • Node.js fs.readFilepeut être appelé dans le futur , lorsque le fichier a été lu avec succès ou a généré une erreur.

Dans tous les cas, nous avons un rappel qui peut s'exécuter dans le futur . C'est ce que nous appelons un flux asynchrone .

L'exécution asynchrone est expulsée du flux synchrone. Autrement dit, le code asynchrone ne s'exécutera jamais pendant l'exécution de la pile de code synchrone. C'est la signification de JavaScript étant monothread.

Plus précisément, lorsque le moteur JS est inactif - n'exécutant pas une pile de (a) code synchrone - il interrogera les événements qui peuvent avoir déclenché des rappels asynchrones (par exemple, expiration du délai d'attente, réponse du réseau reçue) et les exécutera l'un après l'autre. Ceci est considéré comme une boucle d'événement .

Autrement dit, le code asynchrone mis en évidence dans les formes rouges dessinées à la main ne peut s'exécuter qu'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 vous fier à l'exécution d'une fonction asynchrone jusqu'à ce que vous sachiez qu'elle s'est exécutée, et comment faire cela?

C'est simple, vraiment. La logique qui dépend de l'exécution de la fonction asynchrone doit être démarrée / appelée à partir de cette fonction asynchrone. Par exemple, déplacer le alerts et le console.logs trop à l'intérieur de la fonction de rappel produirait le résultat attendu, car le résultat est disponible à ce stade.

Implémentation de votre propre logique de rappel

Souvent, vous devez faire plus de choses avec le résultat d'une fonction asynchrone ou faire des choses différentes avec le résultat en fonction de l'endroit 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 souffre clairement du même problème que les autres exemples, il n'attend pas que la fonction asynchrone s'exécute.

Abordons-le en implémentant notre propre système de rappel. Tout d'abord, on se débarrasse de ce moche outerScopeVarqui est complètement inutile dans ce cas. Ensuite, nous ajoutons un paramètre qui accepte un argument de fonction, notre callback. Lorsque l'opération asynchrone se termine, nous appelons ce rappel en passant le résultat. La mise en œuvre (veuillez 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 démonstratif). 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 s'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 où le rappel a été défini, car les rappels asynchrones sont exécutés longtemps après la fin de l'exécution du code synchrone.

Au lieu de returnsaisir une valeur à partir d'un rappel asynchrone, vous devrez utiliser le modèle de rappel, ou ... Promises.

Promesses

Bien qu'il existe des moyens de garder l' enfer des rappels à distance avec vanilla JS, les promesses gagnent en popularité et sont actuellement normalisées dans ES6 (voir Promise - MDN ).

Les promesses (aka Futures) fournissent une lecture plus linéaire, et donc agréable, du code asynchrone, mais expliquer toute leur fonctionnalité est hors de portée de cette question. Au lieu de cela, je vais laisser ces excellentes ressources pour les intéressés:


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


Remarque: j'ai marqué cette réponse comme Wiki communautaire, donc toute personne ayant au moins 100 réputations peut la modifier et l'améliorer! N'hésitez pas à améliorer cette réponse ou à soumettre une toute nouvelle réponse si vous le souhaitez également.

Je veux transformer cette question en sujet canonique pour répondre aux problèmes d'asynchronisme qui ne sont pas liés à Ajax (il y a Comment renvoyer la réponse d'un appel AJAX? Pour cela), par conséquent, ce sujet a besoin de votre aide pour être aussi bon et utile que possible !

158 Matt May 29 2014 at 16:09

La réponse de Fabrício est parfaite; mais je voulais compléter sa réponse par quelque chose de moins technique, qui se concentre sur une analogie pour aider à expliquer le concept d'asynchronicité .


Une analogie ...

Hier, le travail que je faisais exigeait des informations d'un collègue. Je l'ai appelé; voici comment s'est déroulée la conversation:

Moi : Salut Bob, j'ai besoin de savoir comment nous avons foo 'd 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 super Bob. Rends-moi un coup de fil quand tu auras l'information!

À ce stade, j'ai raccroché le téléphone. Comme j'avais besoin d'informations de Bob pour terminer mon rapport, j'ai laissé le rapport et suis allé prendre un café à la place, puis j'ai rattrapé un e-mail. 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 plutôt déroulée comme ça;

Moi : Salut Bob, j'ai besoin de savoir comment nous avons foo 'd le bar la semaine dernière. Jim veut un rapport là-dessus, 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 super Bob. J'attendrai.

Et je me suis assis là et j'ai attendu. Et j'ai attendu. Et j'ai attendu. Pendant 40 minutes. Ne rien faire d'autre qu'attendre. Finalement, Bob m'a donné l'information, nous avons raccroché et j'ai terminé mon rapport. Mais j'avais perdu 40 minutes de productivité.


Il s'agit d'un comportement asynchrone ou synchrone

C'est exactement ce qui se passe dans tous les exemples de notre question. Le chargement d'une image, le chargement d'un fichier hors du disque et la demande d'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, cependant, JavaScript continuera à exécuter d'autres codes. 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 la fin de l'opération avant d'exécuter tout autre code, cela aurait été 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, ce qui est une opération lente . La fonction de rappel sera exécutée une fois cette opération lente terminée, mais en attendant, JavaScript continuera à traiter les prochaines lignes de code; ie alert(outerScopeVar).

C'est pourquoi nous voyons l'alerte apparaître undefined; car 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 le seul * moyen en 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, il suffit de déplacer le code qui a besoin de la réponse de l'opération!

* Techniquement, vous pouvez également l'utiliser eval(), mais eval()c'est mal à cette fin


Comment faire attendre mon interlocuteur?

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 n'ait mis à jour la variable. Cela conduit à getWidthOfImage()revenir undefinedet à undefinedêtre alerté.

Pour résoudre ce problème, nous devons autoriser la fonction appelant getWidthOfImage()à enregistrer un rappel, puis déplacer l'alerte de la largeur pour être dans 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 avant, notez que nous avons pu supprimer les variables globales (dans ce cas width).

75 JohnnyHK Jan 21 2015 at 06:42

Voici une réponse plus concise pour les personnes qui recherchent une référence rapide ainsi que quelques exemples utilisant des promesses et async / await.

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 consigné dans ce cas car getMessageretourne avant que le setTimeoutrappel ne soit appelé et se met à jour outerScopeVar.

Les deux principales façons de le résoudre utilisent les rappels et les promesses :

Rappels

Le changement ici est qu'il getMessageaccepte 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 flexible que les rappels car elles peuvent être naturellement combinées pour coordonner plusieurs opérations asynchrones. A Promises / A + implémentation standard est nativement fournie 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és

jQuery fournit des fonctionnalités similaires aux promesses avec ses différés.

function getMessage() {
  var deferred = $.Deferred();
  setTimeout(function() {
    deferred.resolve('Hello asynchronous world!');
  }, 0);
  return deferred.promise();
}

getMessage().done(function(message) {
  console.log(message);  
});

asynchroniser / attendre

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:

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();
58 JohannesFahrenkrug Dec 08 2015 at 23:48

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

Les fonctions asynchrones ressemblent à ...

14 Teja Feb 26 2016 at 10:59

Les autres réponses sont excellentes et je veux simplement y apporter une réponse simple. Se limiter aux appels asynchrones jQuery

Tous les appels ajax (y compris le $.getou $.postou $.ajax) sont asynchrones.

Considérant votre exemple

var outerScopeVar;  //line 1
$.post('loldog', function(response) {  //line 2
    outerScopeVar = response;
});
alert(outerScopeVar);  //line 3

L'exécution du code commence à partir de la ligne 1, déclare la variable et déclenche un appel asynchrone sur la ligne 2, (c'est-à-dire la demande de publication) et continue son exécution à partir de la ligne 3, sans attendre que la demande de publication termine son exécution.

Disons que la demande de publication prend 10 secondes pour se terminer, 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

Maintenant, lorsque vous exécutez cela, vous obtiendrez une alerte sur la ligne 3. Attendez maintenant un certain temps jusqu'à ce que vous soyez sûr que la demande de publication a renvoyé une valeur. Ensuite, lorsque vous cliquez sur OK, dans la zone d'alerte, l'alerte suivante imprime 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 attendant les appels asynchrones.

11 TomSebastian Oct 27 2015 at 13:35

Dans tous ces scénarios, outerScopeVarune valeur est modifiée ou affectée de manière asynchrone ou se produit plus tard (attente ou écoute d'un événement), pour lequel l'exécution en cours n'attendra pas .outerScopeVar = undefined

Discutons de chaque exemple (j'ai marqué la partie qui est appelée de manière asynchrone ou retardée pour que certains événements se produisent):

1.

Ici, nous enregistrons un eventlistner qui sera exécuté lors de cet événement particulier, ici le chargement de l'image, puis l'exécution en cours continue avec les lignes suivantes img.src = 'lolcat.png';et alert(outerScopeVar);pendant ce 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 dans tous les exemples suivants - l'événement peut différer.

2.

Ici, l'événement timeout joue le rôle, qui appellera 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 Queuepour l'exécution, ce qui rend le délai garanti.

3.

Cette fois, le rappel ajax.

4.

Node peut être considéré comme un roi du codage asynchrone. Ici, la fonction marquée est enregistrée comme un gestionnaire de rappel qui sera exécuté après la lecture du fichier spécifié.

5.

La promesse évidente (quelque chose sera fait dans le futur) est asynchrone. voir Quelles sont les différences entre Différé, Promise et Futur dans JavaScript?

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