Quel est l'ordre d'exécution dans les promesses javascript
j'aimerais m'expliquer l'ordre d'exécution de l'extrait suivant qui utilise des promesses javascript.
Promise.resolve('A')
.then(function(a){console.log(2, a); return 'B';})
.then(function(a){
Promise.resolve('C')
.then(function(a){console.log(7, a);})
.then(function(a){console.log(8, a);});
console.log(3, a);
return a;})
.then(function(a){
Promise.resolve('D')
.then(function(a){console.log(9, a);})
.then(function(a){console.log(10, a);});
console.log(4, a);})
.then(function(a){
console.log(5, a);});
console.log(1);
setTimeout(function(){console.log(6)},0);
le résultat est:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
je suis curieux au sujet de l'ordre d'exécution 1 2 3 7... pas les valeurs 'A', 'B'...
si je comprends bien, si une promesse est résolue, la fonction 'then' est placée dans la file d'attente des événements du navigateur. Donc, je m'attendais à 1 2 3 4 ...
@jfriend00 Merci beaucoup pour les explications détaillées! C'est vraiment une énorme quantité de travail!
2 réponses
commentaires
tout d'abord, exécuter des promesses à l'intérieur d'un gestionnaire .then()
et ne pas retourner ces promesses de l'appel .then()
crée une séquence de promesses complètement nouvelle qui n'est pas synchronisée avec les promesses parentes de quelque façon que ce soit. Habituellement, c'est un bug et, en fait, certains moteurs de promesse préviennent réellement quand vous faites cela parce que ce n'est presque jamais le comportement désiré. La seule fois où on voudrait pour faire cela est quand vous faites une sorte de feu et oubliez l'opération où vous ne vous souciez pas des erreurs et vous ne vous souciez pas de synchroniser avec le reste du monde.
ainsi, toutes vos promesses Promise.resolve()
à l'intérieur des handlers .then()
créent de nouvelles chaînes de promesses qui fonctionnent indépendamment de la chaîne mère. Vous n'avez pas un comportement déterminé. C'est comme lancer 4 appels ajax en parallèle. Vous ne savez pas lequel des deux sera terminé en premier. Maintenant, depuis tout votre code à l'intérieur de ces gestionnaires Promise.resolve()
se trouve être synchrone (puisque ce n'est pas du code du monde réel), alors vous pourriez obtenir un comportement cohérent, mais ce n'est pas le point de conception des promesses donc je ne passerais pas beaucoup de temps à essayer de comprendre quelle chaîne Promise qui exécute du code synchrone seulement va finir en premier. Dans le monde réel, ça n'a pas d'importance parce que si l'ordre est important, alors vous ne laisserez pas les choses au hasard de cette façon.
résumé
-
tous les manipulateurs 151970920 "sont appelés de manière asynchrone après le fil courant des finitions d'exécution (comme le dit le Promises/a+ spec, quand le moteur JS retourne au"code de la plate-forme"). Cela est vrai même pour les promesses qui sont résolues de manière synchrone comme
Promise.resolve().then(...)
. Ceci est fait pour la cohérence de la programmation de sorte qu'un gestionnaire.then()
est constamment appelé asynchrone peu importe que la promesse soit résolue ou non. immédiatement ou plus tard. Cela évite certains bugs de synchronisation et rend plus facile pour le code appelant de voir l'exécution asynchrone cohérente. -
il n'y a pas de spécification qui détermine l'ordre relatif des manipulateurs
setTimeout()
par rapport aux manipulateurs programmés.then()
si les deux sont en file d'attente et prêts à fonctionner. Dans votre implémentation, un handler en attente.then()
est toujours exécuté avant un handler en attentesetTimeout()
, mais la spécification Promises/a+ spec dit ce n'est pas déterminée. Il est dit que les handlers.then()
peuvent être programmés de plusieurs façons, dont certaines fonctionneraient avant les appels en attentesetTimeout()
et d'autres après les appels en attentesetTimeout()
. Par exemple, la spécification Promises/a+ permet aux gestionnaires.then()
d'être programmés avec soitsetImmediate()
qui s'exécute avant les appelssetTimeout()
en attente ou avecsetTimeout()
qui s'exécute après les appelssetTimeout()
en attente. Ainsi, votre code ne devrait pas dépendre qui commande à tous. -
plusieurs chaînes de promesses indépendantes n'ont pas d'ordre d'exécution prévisible et vous ne pouvez pas compter sur un ordre particulier. C'est comme lancer quatre appels ajax en parallèle où vous ne savez pas lequel sera complété en premier.
-
si l'ordre d'exécution est important, Ne créez pas une course qui dépend des moindres détails de mise en œuvre. À la place, link promise des chaînes pour forcer un ordre d'exécution particulier.
-
vous ne voulez généralement pas créer des chaînes de promesses indépendantes dans un handler
.then()
qui ne sont pas retournés par le handler. Il s'agit généralement d'un bug, sauf dans de rares cas de feu et d'oubli sans traitement des erreurs.
Ligne Par Ligne Analyse
"Donc, voici une analyse de votre code. J'ai ajouté des numéros de ligne et nettoyé l'indentation pour la rendre plus facile à discuter:
1 Promise.resolve('A').then(function (a) {
2 console.log(2, a);
3 return 'B';
4 }).then(function (a) {
5 Promise.resolve('C').then(function (a) {
6 console.log(7, a);
7 }).then(function (a) {
8 console.log(8, a);
9 });
10 console.log(3, a);
11 return a;
12 }).then(function (a) {
13 Promise.resolve('D').then(function (a) {
14 console.log(9, a);
15 }).then(function (a) {
16 console.log(10, a);
17 });
18 console.log(4, a);
19 }).then(function (a) {
20 console.log(5, a);
21 });
22
23 console.log(1);
24
25 setTimeout(function () {
26 console.log(6)
27 }, 0);
ligne 1 démarre une chaîne promise et y attache un handler .then()
. Puisque Promise.resolve()
se résout immédiatement, la bibliothèque Promise programmera le premier handler .then()
pour courir après ce thread de finitions Javascript. Dans les bibliothèques promises compatibles Promises/a+, tous les gestionnaires .then()
sont appelés asynchrones après le fil courant de l'exécution se termine et lorsque JS retourne à la boucle de l'événement. Cela signifie que n'importe quel autre code synchrone dans ce thread tel que votre console.log(1)
s'exécute suivant ce qui est ce que vous voyez.
Tous les autres .then()
gestionnaires au plus haut niveau ( lignes 4, 12, 19 ) de la chaîne après la première et sera exécuté qu'après la première obtient son tour. Ils sont essentiellement en file d'attente à ce stade.
depuis le setTimeout()
est aussi dans ce fil d'exécution initial, il est exécuté et donc programmé.
Qui est la fin de l'exécution synchrone. Maintenant, le moteur JS démarre les choses qui sont programmées dans la file d'attente d'événements.
pour autant que je sache, il n'y a pas de garantie qui vient en premier un setTimeout(fn, 0)
ou un .then()
handler qui sont tous deux programmés pour fonctionner juste après ce fil de l'exécution. Les manipulateurs .then()
sont considérés "micro-tâches "donc il ne me surprend pas qu'ils courent d'abord avant le setTimeout()
. Mais, si vous avez besoin d'une commande particulière, alors vous devriez écrire un code qui garantit une commande plutôt que de vous reposer sur ce détail d'implémentation.
quoi qu'il en soit, le manipulateur .then()
défini sur ligne 1 court ensuite. Ainsi vous voyez la sortie 2 "A"
de cette console.log(2, a)
.
ensuite, depuis le précédent .then()
handler retourné un en clair, cette promesse est considérée comme résolue de sorte que le handler .then()
défini sur ligne 4 exécute. Voici où vous créez une autre chaîne de promesses indépendante et introduisez un comportement qui est habituellement un bogue.
ligne 5 , crée une nouvelle chaîne Promise. Il résout cette promesse initiale et programme ensuite deux gestionnaires .then()
à exécuter lorsque le fil courant d'exécution est terminé. Dans ce fil courant de l'exécution est le console.log(3, a)
sur la ligne 10 donc c'est pour ça que vous voyez ça après. Ensuite, ce fil de l'exécution se termine et il retourne à l'ordonnanceur pour voir ce qu'il faut exécuter ensuite.
nous avons maintenant plusieurs .then()
handlers dans la file d'attente en attente de la prochaine course. Il y en a un que nous venons de programmer sur la ligne 5 et il y en a un autre dans la chaîne de niveau supérieur sur la ligne 12. Si vous aviez fait cela sur ligne 5 :
return Promise.resolve.then(...)
alors vous auriez lié ces promesses ensemble et elles seraient coordonnées dans l'ordre. Mais, en ne retournant pas la valeur promise, vous avez commencé une toute nouvelle chaîne promise qui n'est pas coordonnée avec la promesse extérieure de niveau supérieur. Dans votre cas particulier, l'ordonnanceur de promesse décide d'exécuter le handler plus profondément imbriqué .then()
suivant. Je ne sais pas honnêtement si c'est par Spécification, Par convention ou juste un détail de mise en œuvre d'un moteur de promesse contre l'autre. Je dirais que si l'ordre est critique pour vous, alors vous devriez forcer un ordre en liant des promesses dans un ordre spécifique plutôt que de compter sur qui gagne la course pour courir en premier.
quoi qu'il en soit, dans votre cas, il s'agit d'une course de planification et le moteur que vous utilisez décide d'exécuter le manipulateur interne .then()
qui est défini sur la ligne 5 suivante et donc vous voyez le 7 "C"
spécifié sur ligne 6 . Il retourne ensuite rien jusqu'à la valeur résolue de cette promesse devient undefined
.
retour dans le scheduler, il exécute le .then()
handler sur ligne 12 . Il s'agit encore une fois d'une course entre ce handler .then()
et celui de ligne 7 qui est également en attente pour courir. Je ne sais pas pourquoi il choisit l'un sur l'autre ici autre que de dire qu'il peut être indéterminé ou varier par moteur promesse parce que l'ordre n'est pas spécifié par le code. Dans tous les cas, le manipulateur .then()
ligne 12 commence à courir. Cela crée à nouveau une nouvelle chaîne de promesse indépendante ou non synchronisée la chaîne précédente. Il programme à nouveau un handler .then()
et vous obtenez le 4 "B"
à partir du code synchrone de ce handler .then()
. Tout le code synchrone est fait dans ce handler donc maintenant, il retourne à l'ordonnanceur pour la tâche suivante.
de retour dans l'ordonnanceur, il décide d'exécuter le .then()
handler on ligne 7 et vous obtenez 8 undefined
. La promesse là est undefined
parce que le précédent .then()
handler dans cette chaîne n'a rien retourné, donc sa valeur de retour était undefined
, donc c'est la valeur résolue de la chaîne promise à ce point.
à ce point, la sortie jusqu'à présent est:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
encore une fois, tout le code synchrone est fait donc il retourne à l'ordonnanceur à nouveau et il décide d'exécuter le .then()
gestionnaire de défini sur ligne 13 . Cela fonctionne et vous obtenez la sortie 9 "D"
et puis il retourne à l'ordonnanceur à nouveau.
conformément à la chaîne précédemment emboîtée Promise.resolve()
, l'annexe choisit d'exécuter le manipulateur externe suivant .then()
défini sur ligne 19 . Il tourne et vous obtenez la sortie 5 undefined
. C'est encore undefined
parce que le précédent .then()
handler dans ce chain n'a pas rendu une valeur, donc la valeur résolue de la promesse était undefined
.
Comme ce point, la sortie jusqu'à présent est:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
à ce point, il n'y a qu'un seul .then()
handler programmé pour être exécuté donc il exécute celui défini sur ligne 15 et vous obtenez la sortie 10 undefined
suivante.
puis, enfin ,le setTimeout()
s'exécute et la sortie finale est:
1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6
si l'on essayait de prédire exactement l'ordre dans lequel cela se produirait, alors il y aurait deux questions principales.
-
Comment êtes en attente
.then()
gestionnaires de priorité par rapport àsetTimeout()
appels qui sont également en attente. -
comment le moteur de promesse décide-t-il de prioriser plusieurs manipulateurs
.then()
qui sont tous en attente de fonctionnement. Selon vos résultats avec ce code il n'est pas de la FIFO.
pour la première question, Je ne sais pas s'il s'agit d'une spécification ou simplement d'un choix d'implémentation ici dans le moteur promise/JS, mais l'implémentation dont vous avez parlé semble prioriser tous les gestionnaires en attente .then()
avant tout appel setTimeout()
. Votre cas est un peu bizarre parce que vous n'avez pas d'appels réels de L'API async à part spécifier .then()
handlers. Si vous aviez une opération asynchrone qui en fait, il a fallu du temps réel pour exécuter au début de cette chaîne promise, puis votre setTimeout()
s'exécuterait avant le handler .then()
sur l'opération réelle async juste parce que l'opération réelle async prend du temps réel pour exécuter. Donc, c'est un peu un exemple artificiel et ce n'est pas le cas de conception habituel pour du vrai code.
pour la deuxième question, j'ai vu une discussion qui discute comment en attente .then()
handlers à différents niveaux de nidification devrait être une priorité. Je ne sais pas si cette discussion a été résolue dans une spécification ou non. Je préfère coder de manière à ce que ce niveau de détail ne m'importe pas. Si je me soucie de l'ordre de mes opérations asynchrones, puis-je relier ma promesse chaînes de contrôler l'ordre et ce niveau de détail de l'implémentation ne m'affecte en aucune façon. Si Je ne me soucie pas de l'ordre, alors je ne me soucie pas de l'ordre donc encore une fois que le niveau de détail de la mise en œuvre ne m'affecte pas. Même si ce était dans une certaine spécification, il semble comme le type de détail qui ne devrait pas être fait confiance à travers de nombreuses implémentations différentes (différents navigateurs, différents moteurs de promesse) à moins que vous aviez testé partout où vous alliez exécuter. Donc, je recommande de ne pas compter sur un ordre d'exécution spécifique quand vous avez des chaînes de promesses non synchronisées.
vous pourriez rendre l'ordre déterminé à 100% en liant simplement toutes vos chaînes promises comme ceci
Promise.resolve('A').then(function (a) {
console.log(2, a);
return 'B';
}).then(function (a) {
var p = Promise.resolve('C').then(function (a) {
console.log(7, a);
}).then(function (a) {
console.log(8, a);
});
console.log(3, a);
// return this promise to chain to the parent promise
return p;
}).then(function (a) {
var p = Promise.resolve('D').then(function (a) {
console.log(9, a);
}).then(function (a) {
console.log(10, a);
});
console.log(4, a);
// return this promise to chain to the parent promise
return p;
}).then(function (a) {
console.log(5, a);
});
console.log(1);
setTimeout(function () {
console.log(6)
}, 0);
cela donne la sortie suivante en Chrome:
1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6
et, puisque la promesse a toutes été enchaînée, l'ordre de la promesse est entièrement défini par le code. La seule chose qui reste comme détail d'implémentation est le timing du setTimeout()
qui, comme dans votre exemple, vient en dernier, après tout en attente .then()
handlers.
Edit:
après examen du promesses / a + Spécification , nous trouvons ceci:
2.2.4 onFulfilled or onRejected ne doit pas être appelé tant que la pile de contexte d'exécution ne contient que du code de plate-forme. [3.1].
....
3.1 ici, "code de plate-forme" signifie moteur, environnement et promesse. la mise en œuvre du code. Dans la pratique, cette exigence garantit que: onFulfilled et onRejected exécuter de manière asynchrone, après l'événement tour de boucle dans lequel puis est appelé, et avec une nouvelle pile. Cela peut être mis en œuvre avec un mécanisme de "macro-tâche" tel que setTimeout ou setImmediate, ou avec un mécanisme de "micro-tâche" tel que MutationObserver ou processus.nextTick. Depuis la mise en œuvre de la promesse est considéré comme le code de la plate-forme, il peut lui-même contenir un calendrier des tâches file d'attente ou "trampoline" dans lequel les manipulateurs sont appelés.
cela dit que les gestionnaires .then()
doivent exécuter asynchrone après que la pile d'appels retourne au code de la plate-forme, mais laisse entièrement à l'implémentation comment faire exactement que ce soit fait avec une macro-tâche comme setTimeout()
ou une micro-tâche comme process.nextTick()
. Ainsi, selon cette spécification, elle n'est pas déterminée et ne devrait pas être invoquée.
Je ne trouve aucune information concernant les macro-tâches, les micro-tâches ou le timing des manipulateurs de promesse .then()
par rapport à setTimeout()
dans la spécification ES6. Cela n'est peut-être pas surprenant puisque setTimeout()
ne fait pas partie de la spécification ES6 (c'est une fonction d'environnement hôte, pas une caractéristique de langue).
Je n'ai trouvé aucune spécification pour étayer cela, mais les réponses à cette question différence entre microtask et macrotask dans un contexte de boucle d'événement expliquer comment les choses ont tendance à fonctionner dans les navigateurs avec macro-tâches et micro-tâches.
POUR INFORMATION, Si vous voulez plus d'informations sur les micro-tâches et macro-tâches, voici un article de référence intéressant sur le sujet: tâches, microtasks, Files d'attente et horaires .
le moteur JavaScript du navigateur a quelque chose appelé la"boucle d'événement". Il n'y a qu'un seul thread de code JavaScript tournant à la fois. Quand un bouton est cliqué ou une requête AJAX ou autre chose asynchrone complète, un nouvel événement est placé dans la boucle d'événement. Le navigateur exécute ces événements, un à la fois.
ce que vous regardez ici est que vous exécutez du code qui s'exécute de manière asynchrone. Lorsque le code asynchrone est terminé, il ajoute un événement à la boucle d'événements. L'ordre dans lequel les événements sont ajoutés dépend de la durée de chaque opération asynchrone.
cela signifie que si vous utilisez quelque chose comme AJAX où vous n'avez aucun contrôle sur l'ordre dans lequel les requêtes seront complétées, vos promesses peuvent être exécutées dans un ordre différent à chaque fois.