JavaScript の Promise: 概要

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

デベロッパーの皆様、ウェブ開発の歴史における重要な瞬間が近づいています。

[ドラムロール開始]

JavaScript に Promise が登場しました。

[花火が爆発し、上からキラキラした紙が降り注ぎ、群衆が熱狂している]

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

  • 周囲の人々が歓声を上げているが、何が起こっているのかわからない。「約束」とは何かさえわからないかもしれません。肩をすくめたいところですが、キラキラした紙の重みが肩にのしかかっています。もしそうなら、心配しないでください。私も、なぜこのことを気にする必要があるのかを理解するのに長い時間がかかりました。おそらく、最初から始めることをおすすめします。
  • 空気をパンチします。そろそろですよね?Promise のようなものを以前に使用したことはあるが、実装によって API が少しずつ異なるのが気になる。公式の JavaScript バージョンの API は何ですか?まずは用語集から始めることをおすすめします。
  • あなたはすでにこのことを知っており、このことをニュースのように騒ぎ立てる人たちを嘲笑しています。自分の優位性を実感したら、すぐに API リファレンスに進んでください。

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

Browser Support

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

Source

完全な Promise 実装がないブラウザを仕様に準拠させるため、または他のブラウザや Node.js に Promise を追加するには、ポリフィル(2k gzipped)をご覧ください。

何が問題なのですか?

JavaScript はシングル スレッドです。つまり、2 つのスクリプトを同時に実行することはできません。スクリプトは 1 つずつ実行する必要があります。ブラウザでは、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 - Promise に関連するアクションが失敗しました
  • pending - まだ履行または拒否されていない
  • settled - 履行済みまたは不承認

この仕様では、then メソッドを持つオブジェクトを説明するために、thenable という用語も使用しています。この用語は、元イングランド代表監督の Terry Venables を連想させるため、できるだけ使用しないようにします。

JavaScript に 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 を使用します。これは、Quota ManagementFont Load EventsServiceWorkerWeb MIDIStreams などですでに実施されています。

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

JavaScript Promise API は、then() メソッドを持つものを Promise のようなもの(Promise の言葉で言うと thenable sigh)として扱うため、Q Promise を返すライブラリを使用しても問題ありません。新しい JavaScript Promise とうまく連携します。

ただし、前述のとおり、jQuery の Deferred は少し役に立ちません。幸いなことに、これらは標準の 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 は最初の 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() は単一の引数を取り、変換された値を返すため、次のようにショートカットを作成できます。

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

エラー処理

前述のとおり、then() は 2 つの引数(成功と失敗)を取ります(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() ブロックに移動します。上記のフローチャートを次に示します(フローチャートが好きなので)。

青い線は解決された 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);
})

エラー処理の実践

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

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 をページに追加するコールバックも含まれます。代わりに、キャッチ コールバックに移動します。その結果、以前の操作のいずれかが失敗した場合、ページに「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 を作成します。Promise のインスタンスを渡すと、単にそれを返します(注: これは仕様の変更であり、まだ一部の実装では対応していません)。Promise のようなもの(then() メソッドがある)を渡すと、同じように解決/拒否する真の Promise が作成されます。他の値(Promise.resolve('Hello') の場合、その値で解決される Promise が作成されます。上記のように値なしで呼び出すと、「undefined」で解決されます。

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

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 は、配列を単一の値(この場合は 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 倍になります。さらに重要なのは、コードの追跡が難しくなることです。ただし、他の ES6 機能と組み合わせると、さらに簡単に使用できるようになります。

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

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

校正と修正/推奨事項の作成にご協力いただいた Anne van Kesteren、Domenic Denicola、Tom Ashworth、Remy Sharp、Addy Osmani、Arthur Evans、平野 豊に感謝いたします。

また、記事のさまざまな部分を更新してくれた Mathias Bynens にも感謝します。