Question Pourquoi ma variable est-elle inchangée après que je l'ai modifiée dans une fonction? - Référence de code asynchrone


Étant donné les exemples suivants, pourquoi outerScopeVar 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 ça sort undefined dans tous ces exemples? Je ne veux pas de solutions de contournement, je veux savoir Pourquoi Cela arrive.


Remarque: Ceci est une question canonique pour Asynchronicité JavaScript. N'hésitez pas à améliorer cette question et à ajouter des exemples plus simples avec lesquels la communauté peut s'identifier.


543
2018-05-14 23:55


origine


Réponses:


Un mot de réponse: asynchronicité.

Avant-propos

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


La réponse à la question posée

Trouvons le comportement commun en premier. Dans tous les exemples, outerScopeVar est modifié à l'intérieur d'un fonction. Cette fonction n'est clairement pas exécutée immédiatement, elle est affectée ou passée en argument. C'est ce qu'on appelle un rappeler.

Maintenant, la question est: quand est appelé ce rappel?

Cela dépend du cas. Essayons de retracer un comportement commun:

  • img.onload peut être appelé parfois dans le futur, quand (et si) l'image a été chargée avec succès.
  • setTimeout peut être appelé parfois dans le futur, après que le délai a expiré et que le délai n'a pas été annulé par clearTimeout. Remarque: même en utilisant 0 En tant que délai, tous les navigateurs ont un délai de temporisation minimal (spécifié à 4 ms dans la spécification HTML5).
  • jQuery $.postLe rappel peut être appelé parfois dans le futur, quand (et si) la requête Ajax a été terminée avec succès.
  • Node.js fs.readFile peut être appelé parfois 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 parfois dans le futur. Ce "quelque temps dans le futur" est ce que nous appelons écoulement asynchrone.

L'exécution asynchrone est expulsée du flux synchrone. Autrement dit, le code asynchrone sera jamais s'exécuter pendant l'exécution de la pile de code synchrone. C'est la signification de JavaScript étant mono-thread.

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

C'est-à-dire que le code asynchrone mis en évidence dans les formes rouges dessinées à la main ne peut s'exécuter qu'après que tous les codes synchrones restants dans leurs blocs de code respectifs ont été exécutés:

async code highlighted

En résumé, les fonctions de rappel sont créées de manière synchrone mais exécutées de manière asynchrone. Vous ne pouvez pas compter sur l'exécution d'une fonction asynchrone jusqu'à ce que vous sachiez qu'il a été exécuté, 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 depuis cette fonction asynchrone. Par exemple, déplacer le alertle sable console.logs aussi à l'intérieur de la fonction de rappel produirait le résultat attendu, car le résultat est disponible à ce point.

Implémenter 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);
}

Remarque: j'utilise setTimeout avec un délai aléatoire comme une fonction asynchrone générique, le même exemple s'applique à Ajax, readFile, onload et tout autre flux asynchrone.

Cet exemple souffre clairement du même problème que les autres exemples, il n'attend pas jusqu'à ce que la fonction asynchrone s'exécute.

Faisons face à la mise en œuvre d'un système de rappel de notre part. Tout d'abord, nous nous débarrassons de cette laide outerScopeVar ce qui 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 callback 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);
}

Le plus souvent dans les cas d'utilisation réelle, l'API DOM et la plupart des bibliothèques fournissent déjà la fonctionnalité de rappel (la helloCatAsync implémentation dans cet exemple démonstratif). Vous avez seulement besoin de passer la fonction de rappel et de comprendre qu'il va s'exécuter hors du flux synchrone, et restructurer votre code pour s'adapter à cela.

Vous remarquerez également qu'en raison de la nature asynchrone, il est impossible de return une valeur d'un flux asynchrone vers le flux synchrone où le rappel a été défini, car les rappels asynchrones sont exécutés longtemps après l'exécution du code synchrone.

Au lieu de returnEn utilisant une valeur d'un rappel asynchrone, vous devrez utiliser le modèle de rappel, ou ... Promesses.

Promesses

Bien qu'il existe des moyens de garder le callback hell à la vanille JS, les promesses gagnent en popularité et sont actuellement normalisées en ES6 (voir Promesse - MDN).

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


Plus de documents sur l'asynchronicité de JavaScript

  • L'art du nœud - rappels explique très bien le code asynchrone et les callbacks avec des exemples JS vanilla et du code Node.js.

Remarque:J'ai marqué cette réponse comme Community Wiki, 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 soumettez une toute nouvelle réponse si vous le souhaitez également.

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


441
2018-01-20 23:42



La réponse de Fabrício est sur place; 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 de l'asynchronisme.


Une analogie ...

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

Moi: Salut Bob, j'ai besoin de savoir comment nous foo'd le barLa 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 sonnerie quand vous aurez l'information!

À ce stade, j'ai raccroché le téléphone. Comme j'avais besoin de l'information de Bob pour compléter mon rapport, j'ai quitté le rapport et je suis allé prendre un café à la place, puis j'ai pris connaissance d'un courriel. 40 minutes plus tard (Bob est lent), Bob m'a rappelé et m'a donné l'information 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 avait été comme ça à la place;

Moi: Salut Bob, j'ai besoin de savoir comment nous foo'd le barLa 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 j'ai attendu. Et attendu. Et attendu. Pour 40 minutes. Ne rien faire d'autre que d'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é.


C'est un comportement asynchrone vs. 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 attendre Pour que ces opérations lentes se terminent, 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 s'exécute autre code en attendant la fin de l'opération lente rend le comportementasynchrone. Si JavaScript avait attendu que l'opération se termine avant d'exécuter tout autre code, cela aurait été synchrone comportement.

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 pour charger lolcat.png, qui est un sloooow opération. La fonction de rappel sera exécutée une fois cette opération lente, mais en attendant, JavaScript continuera à traiter les lignes de code suivantes; c'est à dire. alert(outerScopeVar).

C'est pourquoi nous voyons l'alerte undefined; depuis le alert() est traitée immédiatement, plutôt qu'après le chargement de l'image.

Afin de réparer 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 outerScopeVar variable déclarée comme variable globale.

var img = document.createElement('img');

img.onload = function() {
    var localScopeVar = this.width;
    alert(localScopeVar);
};

img.src = 'lolcat.png';

Tu vas toujours voir un callback est spécifié comme une fonction, car c'est le seul moyen * de JavaScript pour définir du code, mais ne l'exécute que plus tard.

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

* Techniquement, vous pouvez utiliser eval() aussi, mais eval() est le mal dans ce but


Comment puis-je garder mon interlocuteur en attente?

Vous pouvez actuellement avoir un code similaire à ceci;

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 return outerScopeVar arrive immédiatement; avant le onload La fonction de rappel a mis à jour la variable. Cela mène à getWidthOfImage() retour undefined, et undefined être alerté.

Pour résoudre ce problème, nous devons autoriser l'appel de la fonction getWidthOfImage() pour enregistrer un rappel, puis déplacez 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).


118
2017-12-08 16:48



Voici une réponse plus concise pour les personnes qui cherchent une référence rapide ainsi que des 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());

undefined est connecté dans ce cas parce que getMessage retourne avant le setTimeout rappel est appelé et mises à jour outerScopeVar.

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

Rappels

Le changement ici est que getMessage accepte un callback paramè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 combinées naturellement pour coordonner plusieurs opérations asynchrones. UNE Promesses / A + implémentation standard est fourni nativement dans node.js (0.12+) et de nombreux navigateurs actuels, mais est également implémenté dans des bibliothèques comme Oiseau bleu 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 des fonctionnalités similaires aux promesses avec ses reports.

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 inclut le support pour async et await (comme Node.js 7.6+), alors vous pouvez utiliser des promesses de manière synchrone async les fonctions:

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

55
2018-02-26 03:59



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

Les fonctions asynchrones sont comme ...

asynchronous call for coffee


44
2017-10-27 06:35



Les autres réponses sont excellentes et je veux juste donner une réponse directe à cette question. Limiter juste aux appels asynchrones de jQuery

Tous les appels ajax (y compris les $.get ou $.post ou $.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 l'appel asynchrone sur la ligne 2 (c'est-à-dire la demande de publication) et continue son exécution depuis la ligne 3 sans attendre la fin de l'exécution.

Disons que la demande de poste prend 10 secondes à remplir, la valeur de outerScopeVar ne sera réglé 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 ceci, vous obtiendrez une alerte sur la ligne 3. Maintenant, attendez un certain temps jusqu'à ce que vous soyez sûr que la demande de publication a retourné une certaine 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 le scénario de la vie 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é dans le bloc asynchrone ou en attente des appels asynchrones.


11



Dans tous ces scénarios outerScopeVar est modifié ou affecté une valeur asynchrone ou se produisant plus tard (en attendant ou en écoutant qu'un événement se produise), pour lequel l'exécution en cours n'attendra pasDonc, dans tous ces cas, le flux d'exécution actuel entraîne 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.

enter image description here

Ici, nous enregistrons un eventlistner qui sera exécuté sur 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); Pendant ce temps, l'événement peut ne pas se produire. c'est-à-dire, funtion img.onload Attendez que l'image référencée se charge, de manière asynchrone. Cela arrivera à tous les exemples suivants - l'événement peut différer.

2.

2

Ici, l'événement timeout joue le rôle, qui appellera le gestionnaire après l'heure spécifiée. C'est ici 0, mais il enregistre encore un événement asynchrone, il sera ajouté à la dernière position de la Event Queue pour l'exécution, ce qui fait le retard garanti.

3.

enter image description here Cette fois ajax callback.

4.

enter image description here

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 avoir lu le fichier spécifié.

5

enter image description here

Promesse évidente (quelque chose sera fait à l'avenir) est asynchrone. voir Quelles sont les différences entre Deferred, Promise et Future dans JavaScript?

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


8