JavaScript の Promise: 概要

Promise を使用すると、遅延計算と非同期計算を簡素化できます。プロミスは、まだ完了していないオペレーションを表します。

デベロッパーの皆様、ウェブ開発の歴史において重要な瞬間に備えてください。

[ドラムロールが始まる]

JavaScript に Promise が登場しました。

[花火が爆発し、きらめく紙が降り注ぎ、観客が歓声を上げる]

この時点で、お客様は次のいずれかのカテゴリに該当します。

  • 周囲の人々が歓声を上げていますが、何が起きているのかわかりません。「約束」が何なのかさえわからないかもしれません。肩をすくめようとしますが、キラキラした紙の重みが肩にのしかかってきます。心配しないでください。私もこの件に注意を払うべき理由を理解するのにかなり時間がかかりました。最初から始めることをおすすめします。
  • 空気をパンチします。そろそろですね。Promise は以前に使用したことがありますが、すべての実装で API が若干異なるのが気になります。公式の JavaScript バージョンの API は何ですか?用語から始めることをおすすめします。
  • あなたはすでにこのことを知っていて、このニュースを初めて知ったかのように飛び跳ねている人々を嘲笑しています。自分の優越性を誇示した後は、API リファレンスに進みましょう。

ブラウザのサポートとポリフィル

Browser Support

  • Chrome: 32.
  • Edge: 12.
  • Firefox: 29.
  • Safari: 8.

Source

完全なプロミス実装がないブラウザを仕様に準拠させるか、他のブラウザと Node.js にプロミスを追加するには、ポリフィル(2,000 バイト圧縮)をご覧ください。

何が問題なのですか?

JavaScript はシングルスレッドです。つまり、2 つのスクリプトを同時に実行することはできず、次々に実行する必要があります。ブラウザでは、JavaScript はブラウザによって異なるさまざまな負荷とスレッドを共有します。ただし、通常、JavaScript はペイント、スタイルの更新、ユーザー操作の処理(テキストのハイライト表示やフォーム コントロールの操作など)と同じキューにあります。これらのいずれかでアクティビティが発生すると、他のアクティビティが遅延します。

人間はマルチスレッドです。複数の指で入力できます。運転しながら会話することもできます。対処しなければならないブロック機能はくしゃみだけです。くしゃみ中は、現在のすべてのアクティビティを停止する必要があります。運転中に会話をしようとしているときに、特に煩わしいものです。スニージーなコードは書きたくないものです。

これまでは、イベントとコールバックを使用してこの問題を回避していたでしょう。イベントは次のとおりです。

var img1 = document.querySelector('.img-1');

img1.addEventListener('load', function() {
  // woo yey image loaded
});

img1.addEventListener('error', function() {
  // argh everything's broken
});

これはまったく問題ありません。画像を取得し、いくつかのリスナーを追加します。これらのリスナーのいずれかが呼び出されるまで、JavaScript の実行を停止できます。

残念ながら、上記の例では、イベントのリッスンを開始する前にイベントが発生する可能性があります。そのため、画像の「complete」プロパティを使用して回避する必要があります。

var img1 = document.querySelector('.img-1');

function loaded() {
  // woo yey image loaded
}

if (img1.complete) {
  loaded();
}
else {
  img1.addEventListener('load', loaded);
}

img1.addEventListener('error', function() {
  // argh everything's broken
});

ただし、画像をリッスンする前にエラーが発生した画像はキャッチされません。残念ながら、DOM にはそのような方法がありません。また、1 つの画像が読み込まれます。一連の画像が読み込まれたタイミングを知りたい場合は、さらに複雑になります。

イベントが最適な方法とは限らない

イベントは、同じオブジェクトで複数回発生する可能性があるもの(keyuptouchstart など)に適しています。これらのイベントでは、リスナーを接続する前に何が起こったかは重要ではありません。ただし、非同期の成功/失敗の場合は、次のようなことを望みます。

img1.callThisIfLoadedOrWhenLoaded(function() {
  // loaded
}).orIfFailedCallThis(function() {
  // failed
});

// and…
whenAllTheseHaveLoaded([img1, img2]).callThis(function() {
  // all loaded
}).orIfSomeFailedCallThis(function() {
  // one or more failed
});

これが Promise が行うことですが、名前が適切です。HTML 画像要素に Promise を返す「ready」メソッドがあれば、次のようにできます。

img1.ready()
.then(function() {
  // loaded
}, function() {
  // failed
});

// and…
Promise.all([img1.ready(), img2.ready()])
.then(function() {
  // all loaded
}, function() {
  // one or more failed
});

最も基本的な意味で、Promise はイベント リスナーに似ていますが、次の点が異なります。

  • プロミスは 1 回だけ成功または失敗できます。2 回成功または失敗することはできません。また、成功から失敗に切り替えたり、その逆に切り替えたりすることもできません。
  • プロミスが成功または失敗した後に、成功/失敗のコールバックを追加した場合、イベントが先に発生しても、正しいコールバックが呼び出されます。

これは、何かが利用可能になった正確な時刻よりも、結果に応じた対応に重点を置いている非同期の成功/失敗に非常に役立ちます。

Promise の用語

Domenic Denicola がこの記事の最初の下書きの校正を行い、用語について「F」の評価をつけました。先生は私を停学処分にし、States and Fates を 100 回写させ、両親に心配する手紙を書きました。それでも、用語の混同はよくありますが、基本は次のとおりです。

プロミスは次のいずれかです。

  • fulfilled - プロミスに関連するアクションが成功しました
  • rejected - 約束に関連するアクションが失敗しました
  • 保留中 - まだ処理または拒否されていません
  • settled - 解決済み(対応済みまたは不承認)

仕様では、then メソッドがあるという点で Promise に似たオブジェクトを説明するために、thenable という用語も使用されています。この用語は、元イングランド代表サッカー監督の テリー ベナブルズを思い出させるため、できるだけ使用しないことにします。

JavaScript に Promise が登場

Promise は、次のようなライブラリの形で長い間存在してきました。

上記の Promise と JavaScript の Promise は、Promises/A+ という共通の標準化された動作を共有しています。jQuery をご利用の場合は、Deferreds という同様の機能があります。ただし、Deferred は Promise/A+ に準拠していないため、微妙に異なり、有用性が低いことに注意してください。jQuery にも Promise 型がありますが、これは Deferred のサブセットにすぎず、同じ問題があります。

Promise の実装は標準化された動作に従いますが、全体的な API は異なります。JavaScript の Promise は、API が RSVP.js と類似しています。プロミスを作成する方法は次のとおりです。

var promise = new Promise(function(resolve, reject) {
  // do a thing, possibly async, then…

  if (/* everything turned out fine */) {
    resolve("Stuff worked!");
  }
  else {
    reject(Error("It broke"));
  }
});

Promise コンストラクタは、resolve と reject の 2 つのパラメータを持つコールバックという 1 つの引数を取ります。コールバック内で何かを行い(非同期で行うこともできます)、すべて正常に完了した場合は resolve を呼び出し、そうでない場合は reject を呼び出します。

通常の JavaScript の throw と同様に、Error オブジェクトで拒否するのが一般的ですが、必須ではありません。Error オブジェクトの利点は、スタック トレースをキャプチャし、デバッグ ツールをより有用にできることです。

このプロミスの使用方法は次のとおりです。

promise.then(function(result) {
  console.log(result); // "Stuff worked!"
}, function(err) {
  console.log(err); // Error: "It broke"
});

then() は、成功ケースのコールバックと失敗ケースのコールバックの 2 つの引数を受け取ります。どちらも省略可能であるため、成功または失敗の場合のみコールバックを追加できます。

JavaScript Promise は、DOM で「Futures」として始まり、「Promises」に名前が変更され、最終的に JavaScript に移行されました。DOM ではなく JavaScript に配置すると、Node.js などのブラウザ以外の JS コンテキストで使用できるため便利です(コア API で使用されるかどうかは別の問題です)。

これらは JavaScript の機能ですが、DOM でも使用できます。実際、非同期の成功/失敗メソッドを持つ新しい DOM API はすべて、Promise を使用します。これは、割り当て管理フォント読み込みイベントServiceWorkerWeb MIDIストリームなどですでに行われています。

他のライブラリとの互換性

JavaScript Promises API は、then() メソッドを持つものを Promise ライク(Promise 用語では thenable)として扱います。そのため、Q Promise を返すライブラリを使用している場合でも、新しい JavaScript Promise と連携できます。sigh

ただし、前述のように、jQuery の遅延処理は少し役に立ちません。幸い、これらは標準の Promise にキャストできます。これはできるだけ早く行うことをおすすめします。

var jsPromise = Promise.resolve($.ajax('/whatever.json'))

ここで、jQuery の $.ajax は Deferred を返します。then() メソッドがあるため、Promise.resolve() はこれを JavaScript Promise に変換できます。ただし、デフレートがコールバックに複数の引数を渡すこともあります。次に例を示します。

var jqDeferred = $.ajax('/whatever.json');

jqDeferred.then(function(response, statusText, xhrObj) {
  // ...
}, function(xhrObj, textStatus, err) {
  // ...
})

一方、JS の Promise は最初の値以外をすべて無視します。

jsPromise.then(function(response) {
  // ...
}, function(xhrObj) {
  // ...
})

通常、これは望ましいことであり、少なくとも必要なものにアクセスできます。また、jQuery は、Error オブジェクトを拒否に渡すという規則に従っていません。

複雑な非同期コードを簡単に

では、コードを記述してみましょう。たとえば、次のようなことをしたい場合:

  1. 読み込みを示すスピナーを開始する
  2. ストーリーの JSON を取得して、タイトルと各チャプターの URL を取得します。
  3. ページにタイトルを追加する
  4. 各チャプターを取得する
  5. ストーリーをページに追加する
  6. スピナーを停止する

ただし、途中で問題が発生した場合は、ユーザーに伝えてください。この時点でスピナーも停止する必要があります。停止しないと、スピナーは回転し続け、酔いを感じて他の UI にクラッシュします。

もちろん、JavaScript を使用してストーリーを配信することはありません。HTML として提供すると速くなります。ただし、API を扱う場合、このパターンは非常に一般的です。複数のデータ取得を行い、すべて完了したら何かを行います。

まず、ネットワークからデータを取得する方法について説明します。

XMLHttpRequest の Promise 化

古い API は、下位互換性のある方法で可能であれば、Promise を使用するように更新されます。XMLHttpRequest が最有力候補ですが、まずは GET リクエストを行う簡単な関数を作成しましょう。

function get(url) {
  // Return a new promise.
  return new Promise(function(resolve, reject) {
    // Do the usual XHR stuff
    var req = new XMLHttpRequest();
    req.open('GET', url);

    req.onload = function() {
      // This is called even on 404 etc
      // so check the status
      if (req.status == 200) {
        // Resolve the promise with the response text
        resolve(req.response);
      }
      else {
        // Otherwise reject with the status text
        // which will hopefully be a meaningful error
        reject(Error(req.statusText));
      }
    };

    // Handle network errors
    req.onerror = function() {
      reject(Error("Network Error"));
    };

    // Make the request
    req.send();
  });
}

では、この関数を使ってみましょう。

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.error("Failed!", error);
})

これで、XMLHttpRequest を手動で入力しなくても HTTP リクエストを実行できるようになりました。これは、XMLHttpRequest のイライラするキャメルケースを目にすることが少なくなるため、非常に便利です。

チェーン

then() で終わりではありません。then を連結して値を変換したり、追加の非同期アクションを次々と実行したりできます。

値の変換

値を変換するには、新しい値を返すだけです。

var promise = new Promise(function(resolve, reject) {
  resolve(1);
});

promise.then(function(val) {
  console.log(val); // 1
  return val + 2;
}).then(function(val) {
  console.log(val); // 3
})

実用的な例として、次に戻りましょう。

get('story.json').then(function(response) {
  console.log("Success!", response);
})

レスポンスは JSON ですが、現在はプレーン テキストとして受信しています。JSON responseType を使用するように get 関数を変更することもできますが、Promise で解決することもできます。

get('story.json').then(function(response) {
  return JSON.parse(response);
}).then(function(response) {
  console.log("Yey JSON!", response);
})

JSON.parse() は 1 つの引数を取り、変換された値を返すため、ショートカットを作成できます。

get('story.json').then(JSON.parse).then(function(response) {
  console.log("Yey JSON!", response);
})

実際、getJSON() 関数は簡単に作成できます。

function getJSON(url) {
  return get(url).then(JSON.parse);
}

getJSON() は引き続き、URL を取得してレスポンスを JSON として解析する Promise を返します。

非同期アクションのキューイング

then を連結して、非同期アクションを順番に実行することもできます。

then() コールバックから何かを返すと、少し不思議なことになります。値を返すと、その値で次の then() が呼び出されます。ただし、Promise のようなものを返すと、次の then() がそれを待機し、その Promise が解決したとき(成功または失敗したとき)にのみ呼び出されます。次に例を示します。

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  console.log("Got chapter 1!", chapter1);
})

ここでは、story.json に非同期リクエストを行い、リクエストする URL のセットを取得してから、その最初の URL をリクエストします。これが、Promise が単純なコールバック パターンから際立って見えるときです。

チャプターを取得するショートカット メソッドを作成することもできます。

var storyPromise;

function getChapter(i) {
  storyPromise = storyPromise || getJSON('story.json');

  return storyPromise.then(function(story) {
    return getJSON(story.chapterUrls[i]);
  })
}

// and using it is simple:
getChapter(0).then(function(chapter) {
  console.log(chapter);
  return getChapter(1);
}).then(function(chapter) {
  console.log(chapter);
})

getChapter が呼び出されるまでは story.json をダウンロードしませんが、次回 getChapter が呼び出されるとストーリー プロミスを再利用するため、story.json は 1 回だけフェッチされます。Promises のリリース

エラー処理

前述のとおり、then() は 2 つの引数を受け取ります。1 つは成功用、もう 1 つは失敗用です(Promise では、fulfill と reject です)。

get('story.json').then(function(response) {
  console.log("Success!", response);
}, function(error) {
  console.log("Failed!", error);
})

catch() を使用することもできます。

get('story.json').then(function(response) {
  console.log("Success!", response);
}).catch(function(error) {
  console.log("Failed!", error);
})

catch() に特別な点はありません。then(undefined, func) の簡易表現にすぎませんが、読みやすくなっています。上記の 2 つのコードサンプルは動作が異なります。後者は次のコードと同等です。

get('story.json').then(function(response) {
  console.log("Success!", response);
}).then(undefined, function(error) {
  console.log("Failed!", error);
})

違いは微妙ですが、非常に便利です。Promise の拒否は、拒否コールバック(または同等の catch())を使用して次の then() にスキップされます。then(func1, func2) では、func1 または func2 が呼び出されます。両方呼び出されることはありません。ただし、then(func1).catch(func2) では、func1 が拒否された場合、両方が呼び出されます。これは、チェーン内の個別のステップであるためです。次のようにします。

asyncThing1().then(function() {
  return asyncThing2();
}).then(function() {
  return asyncThing3();
}).catch(function(err) {
  return asyncRecovery1();
}).then(function() {
  return asyncThing4();
}, function(err) {
  return asyncRecovery2();
}).catch(function(err) {
  console.log("Don't worry about it");
}).then(function() {
  console.log("All done!");
})

上記のフローは通常の JavaScript の try/catch と非常によく似ています。try 内で発生したエラーは、すぐに catch() ブロックに移動します。上記のフローチャートを以下に示します(私はフローチャートを愛しています)。

青い線は、満たされるプロミスを示し、赤い線は拒否されるプロミスを示します。

JavaScript の例外と Promise

拒否は、Promise が明示的に拒否されたときに発生しますが、コンストラクタ コールバックでエラーがスローされた場合にも暗黙的に発生します。

var jsonPromise = new Promise(function(resolve, reject) {
  // JSON.parse throws an error if you feed it some
  // invalid JSON, so this implicitly rejects:
  resolve(JSON.parse("This ain't JSON"));
});

jsonPromise.then(function(data) {
  // This never happens:
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

つまり、Promise コンストラクタのコールバック内で Promise 関連のすべての処理を行うと、エラーが自動的にキャッチされ、拒否になるため便利です。

then() コールバックでスローされたエラーについても同様です。

get('/').then(JSON.parse).then(function() {
  // This never happens, '/' is an HTML page, not JSON
  // so JSON.parse throws
  console.log("It worked!", data);
}).catch(function(err) {
  // Instead, this happens:
  console.log("It failed!", err);
})

エラー処理の実践

ストーリーとチャプターでは、catch を使用してユーザーにエラーを表示できます。

getJSON('story.json').then(function(story) {
  return getJSON(story.chapterUrls[0]);
}).then(function(chapter1) {
  addHtmlToPage(chapter1.html);
}).catch(function() {
  addTextToPage("Failed to show chapter");
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

story.chapterUrls[0] の取得に失敗した場合(http 500 やユーザーがオフラインの場合など)、以降の成功コールバックはすべてスキップされます。これには、レスポンスを JSON として解析しようとする getJSON() のコールバックも含まれます。また、chapter1.html をページに追加するコールバックもスキップされます。代わりに、catch コールバックに移行します。そのため、上記のいずれかのアクションが失敗した場合は、ページに「Failed to show chapter」と表示されます。

JavaScript の try/catch と同様に、エラーがキャッチされ、後続のコードが続行されるため、スピナーは常に非表示になります。上記は、次の非ブロッキング非同期バージョンになります。

try {
  var story = getJSONSync('story.json');
  var chapter1 = getJSONSync(story.chapterUrls[0]);
  addHtmlToPage(chapter1.html);
}
catch (e) {
  addTextToPage("Failed to show chapter");
}
document.querySelector('.spinner').style.display = 'none'

エラーから復元せずに、ロギング目的でのみ catch() を実行することもできます。これを行うには、エラーを再スローします。これは getJSON() メソッドで行うことができます。

function getJSON(url) {
  return get(url).then(JSON.parse).catch(function(err) {
    console.log("getJSON failed for", url, err);
    throw err;
  });
}

1 つのチャプターを取得できましたが、すべてのチャプターを取得したいです。実現しましょう。

並列処理とシーケンス: 両方のメリットを活用する

非同期を理解するのは簡単ではありません。うまくいかない場合、同期コードのようにコードを記述してみてください。この例の場合は、次のようになります。

try {
  var story = getJSONSync('story.json');
  addHtmlToPage(story.heading);

  story.chapterUrls.forEach(function(chapterUrl) {
    var chapter = getJSONSync(chapterUrl);
    addHtmlToPage(chapter.html);
  });

  addTextToPage("All done");
}
catch (err) {
  addTextToPage("Argh, broken: " + err.message);
}

document.querySelector('.spinner').style.display = 'none'

これで問題ありません。ただし、同期中はダウンロードが行われている間、ブラウザがロックされます。これを非同期で行うには、then() を使用して処理を順番に実行します。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // TODO: for each url in story.chapterUrls, fetch & display
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

では、チャプターの URL をループして順番に取得するにはどうすればよいでしょうか。機能しない:

story.chapterUrls.forEach(function(chapterUrl) {
  // Fetch chapter
  getJSON(chapterUrl).then(function(chapter) {
    // and add it to the page
    addHtmlToPage(chapter.html);
  });
})

forEach は非同期対応ではないため、ダウンロードされた順序でチャプターが表示されます。これは基本的に Pulp Fiction の書き方と同じです。これはパルプ フィクションではありません。修正しましょう。

シーケンスの作成

chapterUrls 配列を Promise のシーケンスに変換します。これは then() を使用して行えます。

// Start off with a promise that always resolves
var sequence = Promise.resolve();

// Loop through our chapter urls
story.chapterUrls.forEach(function(chapterUrl) {
  // Add these actions to the end of the sequence
  sequence = sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
})

Promise.resolve() は初めて登場しました。これは、指定した値に解決するプロミスを作成します。Promise のインスタンスを渡すと、単に返されます(注: これは仕様変更であり、一部の実装ではまだ対応していません)。プロミスのような何か(then() メソッドがあるもの)を渡すと、同じ方法で処理/拒否する本物の Promise が作成されます。他の値(Promise.resolve('Hello') の場合、その値を満たすプロミスが作成されます。値を指定しないで呼び出すと、上記のように「undefined」が返されます。

Promise.reject(val) もあります。これは、指定した値(または undefined)で拒否する Promise を作成します。

array.reduce を使用して、上記のコードを整理できます。

// Loop through our chapter urls
story.chapterUrls.reduce(function(sequence, chapterUrl) {
  // Add these actions to the end of the sequence
  return sequence.then(function() {
    return getJSON(chapterUrl);
  }).then(function(chapter) {
    addHtmlToPage(chapter.html);
  });
}, Promise.resolve())

これは前の例と同じことを行いますが、個別の「シーケンス」変数は必要ありません。配列内の各アイテムに対して、reduce コールバックが呼び出されます。「sequence」は最初の呼び出しでは Promise.resolve() ですが、それ以降の呼び出しでは、前の呼び出しから返された値になります。array.reduce は、配列を単一の値(この場合はプロミス)にまとめるのに非常に便利です。

すべてをまとめてみましょう。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  return story.chapterUrls.reduce(function(sequence, chapterUrl) {
    // Once the last chapter's promise is done…
    return sequence.then(function() {
      // …fetch the next chapter
      return getJSON(chapterUrl);
    }).then(function(chapter) {
      // and add it to the page
      addHtmlToPage(chapter.html);
    });
  }, Promise.resolve());
}).then(function() {
  // And we're all done!
  addTextToPage("All done");
}).catch(function(err) {
  // Catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  // Always hide the spinner
  document.querySelector('.spinner').style.display = 'none';
})

これで、同期バージョンの完全な非同期バージョンが完成しました。ただし、改善の余地はあります。現在、ページは次のようにダウンロードされています。

ブラウザは複数のファイルを一度にダウンロードするのが得意なため、チャプターを次々とダウンロードするとパフォーマンスが低下します。すべてのファイルを同時にダウンロードし、すべてが届いたら処理します。幸い、このための API があります。

Promise.all(arrayOfPromises).then(function(arrayOfResults) {
  //...
})

Promise.all は、Promise の配列を受け取り、それらがすべて正常に完了したときに実行される Promise を作成します。渡した Promise と同じ順序で、結果の配列(Promise が解決した結果)が返されます。

getJSON('story.json').then(function(story) {
  addHtmlToPage(story.heading);

  // Take an array of promises and wait on them all
  return Promise.all(
    // Map our array of chapter urls to
    // an array of chapter json promises
    story.chapterUrls.map(getJSON)
  );
}).then(function(chapters) {
  // Now we have the chapters jsons in order! Loop through…
  chapters.forEach(function(chapter) {
    // …and add to the page
    addHtmlToPage(chapter.html);
  });
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened so far
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

接続によっては、1 つずつ読み込むよりも数秒速く、最初の試行よりもコードが少なくなります。チャプターは任意の順序でダウンロードできますが、画面には正しい順序で表示されます。

ただし、ユーザーが感じるパフォーマンスは改善できます。チャプター 1 が届いたら、ページに追加する必要があります。これにより、残りのチャプターが届く前にユーザーが読み始めることができます。チャプター 3 が公開された場合、チャプター 2 が欠落していることをユーザーが認識していない可能性があるため、チャプター 3 をページに追加しません。チャプター 2 が届いたら、チャプター 2 と 3 を追加できます。

これを行うには、すべてのチャプターの JSON を同時に取得し、シーケンスを作成してドキュメントに追加します。

getJSON('story.json')
.then(function(story) {
  addHtmlToPage(story.heading);

  // Map our array of chapter urls to
  // an array of chapter json promises.
  // This makes sure they all download in parallel.
  return story.chapterUrls.map(getJSON)
    .reduce(function(sequence, chapterPromise) {
      // Use reduce to chain the promises together,
      // adding content to the page for each chapter
      return sequence
      .then(function() {
        // Wait for everything in the sequence so far,
        // then wait for this chapter to arrive.
        return chapterPromise;
      }).then(function(chapter) {
        addHtmlToPage(chapter.html);
      });
    }, Promise.resolve());
}).then(function() {
  addTextToPage("All done");
}).catch(function(err) {
  // catch any error that happened along the way
  addTextToPage("Argh, broken: " + err.message);
}).then(function() {
  document.querySelector('.spinner').style.display = 'none';
})

両方の長所を活かすことができます。すべてのコンテンツを配信する時間は同じですが、ユーザーは最初のコンテンツをより早く受け取ることができます。

この単純な例では、すべてのチャプターがほぼ同時に到着しますが、より多くの大きなチャプターがある場合は、1 つずつ表示するメリットがさらに大きくなります。

Node.js スタイルのコールバックまたはイベントで上記を行うと、コードは約 2 倍になりますが、さらに重要なのは、追跡が容易ではないことです。ただし、これは Promise の終わりではありません。他の ES6 機能と組み合わせると、さらに簡単になります。

ボーナス ラウンド: 機能の拡張

この記事を最初に書いたときから、Promise の使用方法は大幅に拡大されています。Chrome 55 以降、非同期関数により、メインスレッドをブロックすることなく、同期しているかのようにプロミスベースのコードを書くことができます。詳しくは、非同期関数に関する記事をご覧ください。主要なブラウザでは、Promise と非同期関数の両方が広くサポートされています。詳細については、MDN の Promise非同期関数のリファレンスをご覧ください。

校正と修正、推奨事項の提供をしてくれた Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans、Yutaka Hirano の皆様に感謝いたします。

また、Mathias Bynens に記事のさまざまな部分を更新していただきました。