web-dev-qa-db-ja.com

javascript promiseの実行順序は何ですか

Javascript promiseを使用する次のスニペットの実行順序を自分で説明したいと思います。

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

結果は次のとおりです。

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

私は実行順序に興味があります1 2 3 7 ...値 'A'、 'B'ではありません...

私の理解では、約束が解決されると、「then」関数がブラウザのイベントキューに入れられます。だから私の期待は1 2 3 4 ...


@ jfriend00ありがとう、詳細な説明をありがとう!それは本当に膨大な量の仕事です!

29
I.R.

Comments

まず、.then()ハンドラー内でpromiseを実行し、.then()コールバックからこれらのpromiseを返さないと、親Promiseとはまったく同期されないまったく新しい未接続のpromiseシーケンスが作成されます。通常、これはバグであり、実際、いくつかのpromiseエンジンは、それを行うと実際に警告を発します。これは、ほとんど望ましい動作ではないためです。それをしたいと思うのは、エラーを気にせず、他の世界と同期することを気にしない、なんらかの火事をして操作を忘れるときだけです。

したがって、Promise.resolve()ハンドラー内のすべての.then() Promiseは、親チェーンとは独立して実行される新しいPromiseチェーンを作成します。確定的な動作はありません。これは、4つのajax呼び出しを並行して起動するようなものです。どれが最初に完了するかわかりません。これで、これらのPromise.resolve()ハンドラー内のすべてのコードは偶然同期するため(これは実際のコードではないため)、一貫した動作が得られる可能性がありますが、約束の設計ポイントではないので、あまり時間を費やしません同期コードのみを実行するPromiseチェーンが最初に終了することを把握しようとしています。現実の世界では、順序が重要な場合、この方法で物事をチャンスに任せることはないため、問題ではありません。

概要

  1. すべての.then()ハンドラーは、実行の現在のスレッドが終了した後に非同期的に呼び出されます(Promises/A +の仕様にあるように、JSエンジンが「プラットフォームコード」に戻ると)。これは、Promise.resolve().then(...)などの同期的に解決されるpromiseについても当てはまります。これはプログラミングの一貫性のために行われるため、.then()ハンドラーは、約束がすぐに解決されるか後で解決されるかに関係なく、非同期で一貫して呼び出されます。これにより、いくつかのタイミングバグが防止され、呼び出し元のコードが一貫した非同期実行を簡単に確認できるようになります。

  2. setTimeout()ハンドラーとスケジュール済み.then()ハンドラーの相対的な順序を決定する仕様はありません。実装では、保留中の.then()ハンドラーは常に保留中のsetTimeout()の前に実行されますが、Promises/A +仕様の仕様では、これは確定的ではありません。 .then()ハンドラーは、setTimeout()呼び出しが保留される前に実行されるものと、setTimeout()呼び出しが保留された後に実行されるものがあります。たとえば、Promises/A +仕様では、.then()ハンドラーを、setImmediate()呼び出しが保留される前に実行されるsetTimeout()またはsetTimeout()呼び出しが保留された後に実行されるsetTimeout()でスケジュールできます。したがって、コードはその順序にまったく依存しないようにしてください。

  3. 複数の独立したPromiseチェーンには予測可能な実行順序がなく、特定の順序に依存することはできません。どれが最初に完了するかわからないところで、4つのajax呼び出しを並行して起動するようなものです。

  4. 実行順序が重要な場合、実装の詳細に依存する競合を作成しないでください。代わりに、Promiseチェーンをリンクして、特定の実行順序を強制します。

  5. 通常、ハンドラーから返されない.then()ハンドラー内に独立したプロミスチェーンを作成する必要はありません。これは通常、まれな場合を除いてバグであり、エラー処理なしで忘れられます。

行ごとの分析

それで、ここにあなたのコードの分析があります。行番号を追加し、インデントを整理して議論しやすくしました:

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

Line 1は、Promiseチェーンを開始し、.then()ハンドラーをアタッチします。 Promise.resolve()はすぐに解決するため、Promiseライブラリは、このJavascriptのスレッドが終了した後に最初の.then()ハンドラーを実行するようにスケジュールします。 Promises/A +互換のpromiseライブラリでは、すべての.then()ハンドラーは、現在の実行スレッドが終了した後、JSがイベントループに戻るときに非同期に呼び出されます。これは、console.log(1)など、このスレッド内の他の同期コードが次に実行されることを意味します。

トップレベル(lines 4、12、19)にある他のすべての.then()ハンドラーは、最初のハンドラーの後にチェーンし、最初のハンドラーがターンを取得した後にのみ実行されます。これらは基本的にこの時点でキューに入れられます。

setTimeout()もこの実行の初期スレッドにあるため、実行され、タイマーがスケジュールされます。

これで同期実行の終了です。これで、JSエンジンは、イベントキューでスケジュールされたものの実行を開始します。

私が知る限り、最初にsetTimeout(fn, 0)または.then()ハンドラーが最初に来る保証はありません。これらは両方とも、この実行スレッドの直後に実行するようにスケジュールされています。 .then()ハンドラーは「マイクロタスク」と見なされるため、setTimeout()の前に最初に実行されることは驚くことではありません。ただし、特定の順序が必要な場合は、この実装の詳細に依存するのではなく、順序を保証するコードを作成する必要があります。

とにかく、line 1で定義された.then()ハンドラーが次に実行されます。したがって、そのconsole.log(2, a)からの出力2 "A"が表示されます。

次に、前の.then()ハンドラーがプレーンな値を返したため、そのプロミスは解決されたと見なされ、line 4で定義された.then()ハンドラーが実行されます。ここで、別の独立したプロミスチェーンを作成し、通常はバグである動作を導入します。

Line 5は、新しいPromiseチェーンを作成します。その最初の約束を解決し、現在の実行スレッドが完了したときに実行される2つの.then()ハンドラーをスケジュールします。現在の実行スレッドでは、10行目のconsole.log(3, a)にあるため、次に表示されます。その後、この実行スレッドは終了し、スケジューラに戻って次に実行するものを確認します。

キューには、次の実行を待機している.then()ハンドラがいくつかあります。 5行目に予定したものと、12行目の上位チェーンに次のものがあります。line5でこれを行った場合:

return Promise.resolve.then(...)

その後、これらの約束をリンクし、順番に調整します。ただし、promise値を返さないことで、外側の高レベルのpromiseと調整されていないまったく新しいpromiseチェーンを開始しました。特定のケースでは、promiseスケジューラーは、より深くネストされた.then()ハンドラーを次に実行することを決定します。これが仕様によるものなのか、慣例によるものなのか、一方のプロミスエンジンと他方のプロミスエンジンの実装の詳細だけなのか、正直にはわかりません。順序があなたにとって重要である場合、最初に実行するレースに勝った人に頼るのではなく、特定の順序でプロミスをリンクすることで順序を強制する必要があると思います。

とにかく、あなたの場合、それはスケジューリングレースであり、実行中のエンジンは次に行5で定義されている内部.then()ハンドラーを実行することを決定し、したがってで指定された7 "C"行6。その後、何も返さないため、このプロミスの解決された値はundefinedになります。

スケジューラーに戻り、line 12.then()ハンドラーを実行します。これは、その.then()ハンドラーとline 7のハンドラーとの間の競合であり、これも実行を待機しています。コードで順序が指定されていないため、約束エンジンごとに不確定または変化する可能性があると言う以外に、なぜここで一方を選択するのかわかりません。いずれにせよ、line 12.then()ハンドラーは実行を開始します。これにより、以前の新しい独立した、または同期されていないプロミスチェーンラインが再び作成されます。 .then()ハンドラーを再度スケジュールし、その.then()ハンドラーの同期コードから4 "B"を取得します。すべての同期コードはそのハンドラーで実行されるため、次のタスクのためにスケジューラーに戻ります。

スケジューラーに戻ると、line 7.then()ハンドラーを実行することに決定し、8 undefinedを取得します。チェーン内の前の.then()ハンドラーは何も返さなかったため、その約束はundefinedであり、その戻り値はundefinedでした。したがって、それはその時点でのプロミスチェーンの解決された値です。

この時点で、これまでの出力は次のとおりです。

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined

繰り返しますが、すべての同期コードが実行されるため、スケジューラーに戻り、line 13で定義された.then()ハンドラーを実行することを決定します。それが実行され、出力9 "D"を取得し、その後再びスケジューラに戻ります。

以前にネストされたPromise.resolve()チェーンと一致して、スケジュールはline 19で定義された次の外部.then()ハンドラーを実行することを選択します。実行され、出力5 undefinedを取得します。チェーン内の前の.then()ハンドラーが値を返さなかったため、再びundefinedになり、プロミスの解決された値はundefinedになりました。

この時点で、これまでの出力は次のとおりです。

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined

この時点で、実行される予定の.then()ハンドラーは1つだけなので、line 15で定義されたハンドラーを実行し、次に10 undefinedの出力を取得します。

最後に、setTimeout()が実行され、最終的な出力は次のようになります。

1
2 "A"
3 "B"
7 "C"
4 "B"
8 undefined
9 "D"
5 undefined
10 undefined
6

これが実行される順序を正確に予測しようとすると、主に2つの質問があります。

  1. 保留中の.then()ハンドラーと保留中のsetTimeout()呼び出しの優先順位.

  2. Promiseエンジンは、実行をすべて待機している複数の.then()ハンドラーの優先順位をどのように決定しますか。このコードを使用した結果では、FIFOではありません。

最初の質問については、これが仕様ごとか、約束エンジン/ JSエンジンの実装の選択かどうかはわかりませんが、あなたが報告した実装は、.then()呼び出しの前に保留中のsetTimeout()ハンドラをすべて優先するようです。 .then()ハンドラーを指定する以外に実際の非同期API呼び出しがないため、あなたのケースは少し奇妙です。このプロミスチェーンの開始時に実際にリアルタイムで実行する非同期操作がある場合、実際の非同期操作の実行に実際の時間がかかるという理由だけで、setTimeout()は実際の非同期操作の.then()ハンドラーの前に実行されます。したがって、これは少し工夫された例であり、実際のコードの通常の設計事例ではありません。

2番目の質問については、ネストのさまざまなレベルで保留中の.then()ハンドラーに優先順位を付ける方法を説明する議論を見てきました。その議論が仕様書で解決されたかどうかはわかりません。私は、そのレベルの詳細が私にとって重要ではないような方法でコーディングすることを好みます。非同期操作の順序を気にする場合は、順序を制御するためにプロミスチェーンをリンクします。このレベルの実装の詳細は、私にはまったく影響しません。順序を気にしない場合は、順序を気にしないので、実装レベルの詳細は影響しません。これが何らかの仕様であったとしても、実行するすべての場所でテストしない限り、多くの異なる実装(異なるブラウザー、異なるプロミスエンジン)で信頼されるべきではない詳細のタイプのようです。そのため、Promiseチェーンが同期されていない場合、特定の実行順序に依存しないことをお勧めします。


次のようにすべてのプロミスチェーンをリンクするだけで、順序を100%確定できます(親プロミスにリンクされるように内部プロミスを返します)。

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

これにより、Chromeで次の出力が得られます。

1
2 "A"
3 "B"
7 "C"
8 undefined
4 undefined
9 "D"
10 undefined
5 undefined
6

そして、約束はすべて一緒に連鎖されているため、約束の順序はすべてコードによって定義されます。実装の詳細として残っているのは、setTimeout()のタイミングです。これは、例のように、保留中のすべての.then()ハンドラーの後、最後に来ます。

Edit:

Promises/A +仕様 を調べると、これがわかります:

2.2.4実行コンテキストスタックにプラットフォームコードのみが含まれるまで、onFulfilledまたはonRejectedを呼び出さないでください。 [3.1]。

....

3.1ここで、「プラットフォームコード」とは、エンジン、環境、Promise実装コードを意味します。実際には、この要件により、onFulfilledとonRejectedは、イベントループが呼び出された後、それが呼び出され、新しいスタックで非同期に実行されることが保証されます。これは、setTimeoutやsetImmediateなどの「マクロタスク」メカニズム、またはMutationObserverやprocess.nextTickなどの「マイクロタスク」メカニズムで実装できます。 promise実装はプラットフォームコードと見なされるため、ハンドラ自体が呼び出されるタスクスケジューリングキューまたは「トランポリン」を含む場合があります。

これは、.then()ハンドラーは、呼び出しスタックがプラットフォームコードに戻った後に非同期で実行する必要があるが、setTimeout()のようなマクロタスクまたはprocess.nextTick()のようなマイクロタスクで行われたかどうかを、それを行う方法を完全に実装に委ねます。したがって、この仕様に従って、それは決定的ではなく、依存すべきではありません。

ES6仕様では、.then()に関連するマクロタスク、マイクロタスク、またはpromise setTimeout()ハンドラーのタイミングに関する情報は見つかりません。 setTimeout()自体はES6仕様の一部ではないため、これはおそらく驚くことではありません(これは言語機能ではなく、ホスト環境の機能です)。

これをバックアップする仕様は見つかりませんでしたが、この質問への回答 イベントループコンテキスト内のマイクロタスクとマクロタスクの違い -タスクとマイクロタスク。

参考までに、マイクロタスクとマクロタスクに関する詳細情報が必要な場合は、トピックに関する興味深いリファレンス記事があります: タスク、マイクロタスク、キュー、およびスケジュール

74
jfriend00

ブラウザのJavaScriptエンジンには、「イベントループ」と呼ばれるものがあります。一度に実行されるJavaScriptコードのスレッドは1つだけです。ボタンがクリックされるか、AJAXリクエストまたはその他の非同期が完了すると、新しいイベントがイベントループに配置されます。ブラウザはこれらのイベントを一度に1つずつ実行します。

ここで見ているのは、非同期に実行するコードを実行することです。非同期コードが完了すると、適切なイベントがイベントループに追加されます。イベントが追加される順序は、各非同期操作が完了するまでにかかる時間によって異なります。

つまり、リクエストが完了する順序を制御できないAJAXのようなものを使用している場合、Promiseは毎回異なる順序で実行できます。