870 votes

Pourquoi ma variable inchangée après je le modifie à l'intérieur d'une fonction? - Le code asynchrone de référence

Étant donné les exemples suivants, pourquoi est - outerScopeVar undefined 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);

Pourquoi est-elle sortie de "undefined" dans tous ces exemples? Je ne veux pas que des solutions de contournement, je veux savoir pourquoi ce qui se passe.


Note: Ceci est une représentation canonique question pour JavaScript asynchronicité. N'hésitez pas à améliorer cette question et ajouter plus simplifiée des exemples pour lesquels la communauté peut s'identifier.

684voto

Fabrício Matté Points 26309

Un mot de réponse: l'asynchronicité.

Avant-propos

Ce sujet a été réitéré au moins une couple de milliers de fois, ici, dans le Débordement de la Pile. Donc, tout d'abord je tiens à souligner certaines extrêmement ressources utiles:


La réponse à la question à portée de main

Nous allons tracer le comportement courant de la première. Dans tous les exemples, l' outerScopeVar est modifié à l'intérieur d'une fonction. Cette fonction est clairement pas exécuté immédiatement, c'est d'être cédée ou transmise comme argument. C'est ce que nous appelons un rappel.

Maintenant, la question est, quand est-ce callback appelé?

Il dépend du cas. Nous allons essayer de suivre un comportement commun à nouveau:

  • img.onload peut être appelé dans le futur, quand (et si) l'image a été correctement chargé.
  • setTimeout peut être appelé dans le futur, après le délai a expiré et que le délai d'attente n'a pas été annulé par l' clearTimeout. Remarque: même lors de l'utilisation d' 0 que le retard, tous les navigateurs ont un minimum de délai d'attente délai de la pac (spécifié à 4ms dans la spec HTML5).
  • jQuery $.post's de rappel peut être appelé dans le futur, quand (et si) la requête Ajax a été complété avec succès.
  • Node.js' fs.readFile peut être appelé dans le futur, lorsque le fichier a été lu correctement ou jeté une erreur.

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

D'exécution asynchrone est poussé hors du flux synchrone. C'est, du code asynchrone sera jamais exécuter alors que le code synchrone de la pile est en cours d'exécution. C'est le sens de JavaScript étant mono-thread.

Plus précisément, lorsque le moteur JS est inactif -- pas de l'exécution d'une pile de (a)du code synchrone -- il y a une interrogation des événements qui peuvent avoir entraîné des rappels asynchrones (par exemple l'expiration du délai d'attente, reçu de réponse du réseau) et d'exécuter l'un après l'autre. Ceci est considéré comme Boucle d'Événements.

Qui est, le code asynchrone en surbrillance dans le dessin à main rouge formes peuvent s'exécuter qu'après tout le reste du code synchrone dans leurs blocs de code ont signé:

async code highlighted

En bref, les fonctions de rappel sont créés de manière synchrone, mais exécutée de manière asynchrone. Vous ne pouvez pas compter sur l'exécution d'une fonction asynchrone jusqu'à ce que vous savez qu'il a exécuté, et comment le faire?

C'est simple, vraiment. La logique qui dépend de l'asynchrone exécution de la fonction doit être démarré/a appelé à l'intérieur de cette fonction d'asynchrone. Par exemple, le déplacement de l' alerts et console.logs à l'intérieur de la fonction de rappel est sortie le résultat escompté, parce que le résultat est disponible à ce stade.

La mise en œuvre de votre propre logique de rappel

Souvent, vous avez besoin de faire plus de choses avec le résultat d'une fonction asynchrone, ou de faire des choses différentes avec le résultat en fonction de l'endroit où le asynchrones fonction a été appelée. Nous allons aborder un peu plus complexe exemple:

var outerScopeVar;
helloCatAsync();
alert(outerScopeVar);

function helloCatAsync() {
    setTimeout(function() {
        outerScopeVar = 'Nya';
    }, Math.random() * 2000);
}

Note: je suis à l'aide d' setTimeout avec un délai aléatoire comme un générique de fonctions asynchrones, le même exemple s'applique à l'Ajax, readFile, onload et tous les autres flux asynchrone.

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

Nous allons aborder la mise en œuvre d'un rappel de notre propre système. Tout d'abord, nous débarrasser de cette vilaine outerScopeVar qui est complètement inutile dans ce cas. Ensuite, nous ajoutons un paramètre qui accepte un argument de fonction, notre fonction de rappel. Lorsque l'opération asynchrone finitions, nous appelons cette fonction de 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);
}

Le plus souvent dans le réel de cas d'utilisation, l'API DOM et la plupart des bibliothèques offrent déjà la fonctionnalité de rappel ( helloCatAsync mise en œuvre dans ce démonstratif exemple). Vous avez seulement besoin de passer de la fonction de rappel et de comprendre qu'il s'exécute hors de la machine synchrone flux, et de la restructuration de votre code afin de prendre en compte.

Vous remarquerez également que, en raison de la nature asynchrone, il est impossible d' return d'une valeur d'une asynchrones retour du flux de la machine synchrone flux où le rappel a été définie, car les rappels asynchrones sont exécutées de temps après le code synchrone a déjà fini de s'exécuter.

Au lieu de returning une valeur à partir d'un rappel asynchrone, vous aurez à faire usage de la fonction de rappel du motif, ou... des Promesses.

Promesses

Bien qu'il existe des façons de garder le rappel de l'enfer dans la baie avec de la vanille JS, les promesses sont de plus en plus en popularité et sont actuellement mis en œuvre dans les DOM standard (voir la Promesse - MDN).

Promesses (un.k.un. Les contrats à terme) de fournir une plus linéaire, et donc agréable, la lecture de code asynchrone, mais d'expliquer la totalité de leur fonctionnalité est hors de la portée de cette question. Au lieu de cela, je vais laisser ces excellentes ressources pour les intéressés:


En plus du matériel de lecture à propos de JavaScript asynchronicité

  • L'Art du Nœud - Rappels explique le code asynchrone et rappels très bien avec la vanille JS exemples et Node.js de code.

Note: j'ai marqué que cette réponse de la Communauté Wiki, donc n'importe qui avec au moins 100 points de réputation de les modifier et de l'améliorer! N'hésitez pas à améliorer cette réponse, ou de présenter un tout nouveau réponse si vous le souhaitez.

J'aimerais aborder cette question dans un canoniques sujet pour répondre à l'asynchronicité des questions qui ne sont pas liés à l'Ajax (il y a la Façon de retourner la réponse d'un appel AJAX? pour que), d'où ce sujet a besoin de votre aide pour être aussi bon et utile que possible!

178voto

Matt Points 38395

Fabrício la réponse est sur place; mais j'ai voulu compléter sa réponse avec quelque chose de moins technique, qui met l'accent sur une analogie pour expliquer le concept de l'asynchronicité.


Une Analogie...

Hier, le travail que je faisais besoin de quelques informations de la part d'un collègue de la mine. J'ai sonné à lui; voici comment la conversation se poursuivit:

Moi: Salut Bob, j'ai besoin de savoir comment nous foo'd le bar'le d de la semaine dernière. Jim veut un rapport sur elle, et vous êtes le seul qui connaît les détails autour d'elle.

Bob: bien Sûr, mais ça va me prendre environ 30 minutes?

Moi: C'est grand Bob. Donnez-moi un anneau en arrière quand vous avez obtenu les informations!

À ce stade, j'ai raccroché le téléphone. Depuis que j'ai besoin d'informations de Bob pour compléter mon rapport, j'ai quitté le rapport et est allé pour un café de la place, alors je me suis rattrapé sur certains e-mail. 40 minutes plus tard (Bob est lent), Bob rappelé et m'a donné les renseignements dont j'avais besoin. À ce stade, j'ai repris mon travail avec mon rapport, que j'ai eu toutes les informations dont j'avais besoin.


Imaginez si la conversation avait disparu comme cela à la place;

Moi: Salut Bob, j'ai besoin de savoir comment nous foo'd le bar'le d de la semaine dernière. Jim veut un rapport sur elle, et vous êtes le seul qui connaît les détails autour d'elle.

Bob: bien Sûr, mais ça va me prendre environ 30 minutes?

Moi: C'est grand Bob. Je vais attendre.

Et je me suis assis et a attendu. Et attendu. Et attendu. Pendant 40 minutes. Ne rien faire, mais en attente. Finalement, Bob m'a donné l'information, nous avons accroché, et j'ai terminé mon rapport. Mais j'avais perdu 40 minutes de la productivité.


Bienvenue à vs asynchrone synchrone comportement

C'est exactement ce qui se passe dans tous les exemples de notre question. Chargement d'une image, le chargement d'un fichier du disque, et la demande d'une page via AJAX sont toutes les opérations lentes (dans le contexte de l'informatique moderne).

Plutôt que d'attente pour ces opérations pour terminer, JavaScript vous permet de vous inscrire à un rappel qui s'exécute lorsque l'opération est terminée. Dans l'intervalle, le moteur JavaScript s'en va et fait quelque chose d'autre. Ceci est appelé asynchrone comportement. Si JavaScript avait attendu autour, cela aurait été synchrone comportement.

Laissez la carte de notre analogie à l'un des exemples de la question:

// This is my report. It needs completing
var outerScopeVar;

// This is where I make the phone call to Bob
var img = document.createElement('img');

// This is where I ask him for the information
img.src = 'lolcat.png';

// This is where I ask him to ring me back 
img.onload = function() {
    // And when he rings me back, I will do:
    // complete my report with the information I got now
    outerScopeVar = this.width;
};

// But in the meantime, I go and get some coffee. "alert(outerScopeVar);"
// shouldn't be here; this is what is causing the problem with our code.
// We'll fix that below.
alert(outerScopeVar);

Remarque, j'ai déménagé img.src = 'lolcat.png' jusqu'en quelques lignes, d'où il était dans la question. C'est juste pour l'histoire fait sens... ça marchera parfaitement bien là où il était!


Retour à la question initiale...

Si vous regardez l'exemple de code ci-dessus, vous verrez notre alert(outerScopeVar) est dans la partie du code qui se produit immédiatement. C'est pourquoi il n'affiche pas la bonne valeur, car la valeur n'a pas encore été reçue! C'est comme donner Jim la feuille vide juste après avoir raccroché, avant que Bob me rappelle.

Tout ce que nous devons faire, c'est de passer le code dans la fonction de rappel;

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

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

img.src = 'lolcat.png';

Vous aurez toujours voir un rappel étant précisé en tant que fonction, parce que c'est la seule* moyen en JavaScript pour définir un code, mais pas l'exécuter jusqu'à ce que plus tard.

Dans tous nos exemples, l' function() { /* Do something */ } est le rappel; déplacer le code qui a besoin de la réponse de l'opération en il y!

Vous remarquerez également, il y a maintenant pas besoin d' outerScopeVar déclarée comme une variable globale. Votre code peut devenir:

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

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

img.src = 'lolcat.png';

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


Mais j'ai Jim attente pour moi de terminer le rapport. Comment puis-je le garder en attente?

Vous pourriez avoir quelque chose comme ceci pour le moment:

function goAndCreateAReportPlease(onWhat) {
    var outerScopeVar;

    var img = document.createElement('img');
    img.onload = function() {
        outerScopeVar = this.width;
    };
    img.src = 'lolcat.png';
    return outerScopeVar;
}

var report = goAndCreateAReportPlease('how did we foo the bar?');
alert(report);

Cependant, nous savons maintenant que l' return outerScopeVar arrive immédiatement; avant, nous avons eu de la chance pour obtenir les données de Bob. Jim est un rapport incomplet, et n'est pas heureux.

Eh bien, Jim ne veut pas attendre par mon bureau pour moi pour moi de terminer un rapport. Il m'a demandé de le faire, et m'a dit de l'appeler en arrière une fois qu'il a été fait.

Son familier?

C'est vrai... nous avons besoin pour permettre à Jim pour enregistrer un rappel, afin que nous puissions lui dire une fois que le rapport est terminé.

function goAndCreateAReportPlease(onWhat, callback) {
    var img = document.createElement('img');

    // Here's where I'm telling Bob to call me back once he's got the information
    img.onload = function() {
        // This is where I complete my report once I've got my information,
        // and then call back Jim once I've done.
        callback(this.width);
    };

    // Here's where I'm telling Bob the information I need.
    img.src = 'lolcat.png';
}

// Jim asks me to create a report. He passes his callback as a second parameter
goAndCreateAReportPlease('how did we foo the bar?', function (report) {
    // This is where I'm ringing him with the report.
    alert(report);
});

// In the meantime, Jim can go and do something else.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X