JavaScript の Promise: 概要

Promise は、遅延計算と非同期計算を簡素化します。Promise は、まだ完了していないオペレーションを表します。

ウェブ開発の歴史における重要な瞬間に備えましょう。

[ドラムロールの開始]

JavaScript に Promise が加わりました。

[花火が爆発し、キラキラした紙の雨が上方から降り、観客は盛り上がる]

この時点で、次のいずれかのカテゴリに分類できます。

  • 周囲の人々は歓声をあげていますが、何が騒がれているのかはわかりません。「約束」が何なのかわからなかったりするかもしれませんね。さっとさがすだけでも、キラキラした紙の重さが肩にのぼります。もしそうなら、心配しないでください。なぜそんなに重要なのかを考えるまでに、かなりの時間がかかります。最初から始めることをおすすめします。
  • パンチしたね!そろそろね。前にこれらの Promise のものを使用したことがありますが、実装ごとに API が若干異なることに気が付きます。公式 JavaScript バージョンの API を教えてください。用語から始めることをおすすめします。
  • あなたはすでにこのことを知っていて、飛び跳ねる人たちをニュースのように嘲笑します。API リファレンスを参照しながら、自分の強みを身につけましょう。

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

対応ブラウザ

  • 32
  • 12
  • 29
  • 8

ソース

完全な Promise を実装していないブラウザを仕様に準拠させる場合や、他のブラウザや Node.js に Promise を追加する場合は、polyfill(2k を gzip 圧縮)を確認してください。

何が問題なの?

JavaScript はシングル スレッドです。つまり、2 つのスクリプトを同時に実行することはできません。1 つずつ実行する必要があります。ブラウザでは、JavaScript はブラウザごとに異なる他の多くの要素とスレッドを共有します。ただし、一般的に JavaScript は、ペイント、スタイルの更新、ユーザー操作(テキストのハイライト表示、フォーム コントロールの操作など)の処理と同じキューにあります。これらのうちの 1 つの活動が、他の活動を遅らせる。

人間はマルチスレッドです。複数の指で入力したり 運転しながら会話を進めたりできます私たちが対処する必要がある唯一のブロック機能は、くしゃみです。くしゃみの間、現在のすべてのアクティビティを一時停止する必要があります。これはかなりわずらわしいものです 特に運転中に会話するときはくしゃみをするコードを書くのは望ましくありません。

これを回避するために、イベントやコールバックを使用したことがあるかもしれません。イベントは次のとおりです。

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 はイベント リスナーに少し似ていますが、次の点が異なります。

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

これは、非同期の成功/失敗に非常に役立ちます。何かが使用可能になった正確な時刻にはそれほど関心がなく、結果への対応を重視するためです。

Promise の用語

Domenic Denicola の校正がこの記事の最初の下書きを読んで、用語について「F」と評価されました。彼は私を拘留し、私に「States and Fates」を 100 回コピーするよう強要し、親に心配な手紙を書きました。それにもかかわらず、私はまだ多くの用語を混同するものですが、基本的なものは次のとおりです。

Promise には次のようなものがあります。

  • fulfilled - Promise に関連するアクションが成功した
  • rejected - Promise に関連するアクションが失敗した
  • 保留中 - まだ処理も不承認もされていません
  • 決済済み - 履行または不承認

また、仕様では、then メソッドを持つという点で、Promise に似たオブジェクトを表すために thenable という用語を使用しています。この用語はイングランド フットボールの元マネージャー、Terry Venables を思い出すので、できるだけ使用しません。

Promise が JavaScript に実装されます。

Promise は以前から、次のようなライブラリの形で使用されていました。

上記と JavaScript の Promise は、Promises/A+ と呼ばれる共通の標準化された動作を共有します。jQuery ユーザーには、遅延と呼ばれる類似した動作があります。ただし、Deferred は Promise/A+ に準拠していないため、若干異なり、有用性が低く注意してください。jQuery にも Promise 型がありますが、これは Deferred のサブセットにすぎません。同じ問題があります。

Promise の実装は標準化された動作に従いますが、全体的な API は異なります。JavaScript の Promise は API の RSVP.js と同様です。Promise の作成方法は次のとおりです。

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 コンストラクタは、コールバックを 1 つの引数として受け取り、2 つのパラメータ(resolve、reject)を渡します。コールバック内でなんらかの処理(非同期など)を行い、すべてが正常に機能した場合は呼び出しを解決し、それ以外の場合は呼び出しを拒否します。

単純な古い JavaScript の throw と同様に、Error オブジェクトで拒否するのが一般的ですが、必須ではありません。Error オブジェクトの利点はスタック トレースをキャプチャし、デバッグツールの利便性を高めることです。

この Promise の使用方法は次のとおりです。

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

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

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

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

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

JavaScript Promise API は、then() メソッドを含むものをすべて Promise に似たものとして処理します(Promise の言葉では thenable)。そのため、Q Promise を返すライブラリを使用しても問題ありません。新しい JavaScript の Promise で適切に機能します。

ただし、先ほども説明したように、jQuery の Deferreds は少し意味がありません。ありがたいことに、これらを標準の Promise にキャストできます。これは、できるだけ早く行う価値があります。

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

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

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 の確約利用

下位互換性が維持できる場合は、古い 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 を連結して値を変換したり、追加の非同期アクションを 1 つずつ実行したりできます。

値の変換

新しい値を返すだけで、値を変換できます。

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() は単一の引数を取り、変換された値を返すため、次のようなショートカットを作成できます。

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() は引き続き Promise を返します。Promise は URL を取得してレスポンスを JSON として解析します。

非同期アクションをキューに入れる

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 が呼び出されたときにストーリーの Promise を再利用するため、story.json は 1 回だけフェッチされます。さあ、約束です!

エラー処理

前述のように、then() は 2 つの引数を取ります。1 つは成功用、もう 1 つは失敗(Promise の用語では履行と拒否)です。

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 による拒否は、拒否コールバックを持つ次の then()(または同等の catch())にスキップされます。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() ブロックに入ります。次に、上の図をフローチャートで示します(私はフローチャートが大好きなので)。

履行される Promise の場合は青い線を、拒否する Promise の場合は赤い線に従ってください。

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() 内のコールバックが含まれます。また、ページに chap1.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() を使用して、処理を 1 つずつ処理します。

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 は非同期に対応していないため、チャプターはダウンロードした順序で表示されます。これは基本的に『パルプ フィクション』の作成と同じです。これは『パルプ・フィクション』ではないので 修正しましょう

シーケンスを作成する

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 を作成します。Promise のインスタンスを渡すと、それが返されます(注: これは仕様の変更であり、一部の実装はまだ準拠していません)。(then() メソッドがある)Promise に似たものを渡すと、同じ方法で解決または拒否する正規の Promise が作成されます。他の値(例:Promise.resolve('Hello') の場合、その値で履行される Promise を作成します。上記のように、値なしで呼び出すと「未定義」で履行されます。

また、Promise.reject(val) もあります。これは、指定された値(または未定義)で拒否する 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())

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

まとめると、次のようになります。

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

これで、同期バージョンの完全非同期バージョンが完成しました。でももっと良くできるはずです。現在、ページは次のようにダウンロードされています。

ブラウザは一度に複数のものをダウンロードするのに適しているため、チャプターを 1 つずつダウンロードすることでパフォーマンスが低下します。目標は、すべて同時にダウンロードし、すべて到着したときに処理することです。幸い、これを行うための 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 章がないことに気付かない可能性があるため、ページには追加しません。第 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 ベースのコードを同期であるかのように記述できるようになりました。詳しくは、my async functions articleをご覧ください。主要なブラウザで、Promise と非同期関数の両方が幅広くサポートされています。詳細については、MDN の Promise非同期関数のリファレンスをご覧ください。

Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans、Yutaka Hirano に本記事の見直しと提案を行い、心より感謝いたします。

また、Mathias Bynens に記事のさまざまな部分を更新していただいたお礼申し上げます。