前回に引き続き、ECMAScript 2015(ECMAScript 6)で新たに追加されたPromiseについて、その概要を全2回に渡って紹介します。今回は後編です。
前回のおさらい
前回は、こんなふうにPromiseを使うという例を紹介しました。それは、以下のようにAjaxでAPIにアクセスする例でした。
var fetchSomething1 = function() { return new Promise(function(resolve, reject) { // API1にアクセス doAjaxStuff(someOptions1, { success: function(data) { // 成功した場合 resolve(); }, fail: function() { // 何かしらエラーが発生した場合 reject({ message: 'APIにアクセスできませんでした' }); } }); }); };fetchSomething1().then(function() { // A: fetchSomething1の成功時に実行される alert('API1よりデータを取得しました'); }, function(error) { // B: fetchSomething1の失敗時に実行される alert('API1よりデータを取得できませんでした。エラーメッセージ: ' + error.message); });
ここで定義しているfetchSomething1
は、Promiseオブジェクトを返します。AjaxでAPIにアクセスするのが成功した場合、then
の第一引数に指定したfunctionが、失敗した場合、then
の第二引数に指定したfunctionが実行されるのでした。
以降、便宜上、then
の第一引数に指定されたfunctionのことをonFulfilledハンドラ、第二引数に指定されたfunctionのことをonRejectedハンドラと呼ぶことにします。
Promiseインターフェイスの実装例: Fetch API
このPromise、今後はJavaScriptを書いていく上での基本的な知識となっていくことが、ほとんど確実と言ってよいだろうと筆者は考えます。
例えば、WHATWGで策定されているFetch APIは、Promiseをベースに作られています。以下の例では、同階層にあるmemo.txt
の内容をFetch APIを利用して取得、そのファイルの内容をconsole.log
しています。
fetch('./memo.txt') .then(function(response) { response.text().then(function(text) { console.log(text); // memo.txtの内容 }); }, function(error) { console.log(error); });
Fetch APIについての知識がなかったとしても、Promiseに慣れていれば、上記コードがどのような意味を持っているかを想像するのは容易でしょう。今後、ブラウザやECMAScript自体が実装していく非同期に処理を行うAPIについても、Promiseをベースとしていくであろうと想像されます。
なお、このコード内では、response.text()
でレスポンス内容のテキストを取得しています。この処理自体も非同期で実装されており、その結果をthen
で受け取るようになっています。
インターフェースを統一できるのもPromiseのメリット
初めに挙げたAjaxする例ですが、コレ、then
を使って、Ajaxの成功時、失敗時に指定したfunctionを実行させているわけですが、前編で紹介したコールバックを使っても実装可能です。例えばこんなふうにしたっていいじゃないですか。
fetchSomething1(function() { // 成功時の処理 }, function() { // エラー時の処理 });
第一引数に成功時の処理を、第二引数にエラー時の処理を。これでも問題ないです。はたまた、こんなのはどうでしょう。各種オプションを渡せるようにしました。
fetchSomething1(params, method, noCache, { success: function() { // 成功時の処理 }, fail: function() { // エラー時の処理 } });
やりかたはいろいろとあります。どの方法が一番良いのでしょう?それぞれメリット、デメリットがあるかもしれませんが、ひとつ言えることは、各々が、それぞれの方法でバラバラな書き方をしてしまうことがあり得るということです。
Promiseを利用すれば、非同期処理の書き方を統一できます。成功か失敗か。結果が単純にその2つに分かれる非同期処理において、それを担うfunctionがPromiseオブジェクトを返してくれるのであれば、結果を受け取った方は、以降の処理をthen
で書くものと理解することができます。
これはあくまで、そのように書くよう統一しておけば、多くの人が理解しやすいコードにできますというだけのことではあるのですが、PromiseがES6に採用されたことで、非同期処理の扱いには、Promiseをベースにしていくのが基本となっていくことはほとんど確実と言えるのではないでしょうか。
順次処理を行わせる
前回、コールバックを使いAPIに順番にアクセスすると、以下のようになってしまうという例を挙げました。
/* 順番に実行 */fetchSomething1(function() { fetchSomething2(function() { fetchSomething3(function() { fetchSomething4(doSomethingFinally); // 全部終わったら doSomethingFinally() }); }); });
そして先ほど、このfetchSomething1
を、Promiseをベースにした書き方にしてみたわけですが、他のfunctionも同様にPromiseベースで書き直し、それぞれがPromiseオブジェクトを返すようにすると、上記コードは以下のように書き直せます。
fetchSomething1() .then(fetchSomething2) .then(fetchSomething3) .then(fetchSomething4) .then(doSomethingFinally);
何段階にも入れ子になってしまうfunctionよりも、こちらのほうがコードの見通しがよいのではないでしょうか。
fetchSomething1
〜fetchSomething4
はそれぞれ、Promsieオブジェクトを返します。then
に指定されたfunction内でPromiseオブジェクトが返された場合、then
の返す値自体が、そのPromiseオブジェクトとなります。上記を一つずつ分けて書くと、以下のようになります。
var promise1 = fetchSomething1(); var promise2 = promise1.then(fetchSomething2); var promise3 = promise2.then(fetchSomething3); var promise4 = promise2.then(fetchSomething4); promise4.then(doSomethingFinally);
各functionはそれぞれのPromiseを返し、続けて書いた処理は、前のPromiseがfulfilled
になった時に初めて実行されます。fetchSomething2
はfetchSomething1
の返すPromiseがfulfilled
になった時に実行され、fetchSomething3
はfetchSometing2
の返すPromiseがfulfilled
になった時に実行され……と、非同期処理が連鎖するように動作します。
Promiseを使えば、このように連続した非同期処理を簡潔に記述することが可能になります。
同期的な処理も混ぜる
そのように非同期処理をうまく扱えるようにするPromiseですが、その途中に同期的な処理を挟むこともできます。
以下で用意した各functionはそれぞれ、非同期的に扱う必要のない処理たちですが、渡された数値を2倍にして返すdouble
と、渡された数値を3倍にして返すtreble
は、Promiseオブジェクトを返し、計算が終わったらその結果を後続する処理に渡すようにしています。
また、渡された数値をコンソールに表示するだけのfunction、dump
は、console.log
で渡された値を表示したあとは、そのままその数値を返しています。
/* 2倍にする */var double = function(number) { return new Promise(function(resolve) { resolve(number * 2); }); };
/* 3倍にする */
var treble = function(number) { return new Promise(function(resolve) { resolve(number * 3); }); };
/* 表示する */
var dump = function(number) { console.log(number); return number; }
/* 実行 */
double(10) // 102 -> 20 .then(dump) // コンソールに表示: 20 .then(treble) // 203 -> 60 .then(dump) // コンソールに表示: 60 .then(double) // 60*2 -> 120 .then(dump); // コンソールに表示: 120
resolve
に渡した値は、後続する処理で受け取ることができるため、double(10)
の結果は後続するdump
に渡されます。function dump
の内容は、console.log
した後、渡された数値を返すだけです。このように、onFulfilledハンドラ内でPromiseオブジェクト以外が返された場合、新しくPromiseオブジェクトが作成され、返した値ですぐにresolve
されます。例えば、上記コードで最初にdump
している箇所は、処理内容的には以下と同じです。
double(10) // 102 -> 20 .then(function(number) { return new Promise(function(resolve) { console.log(number); // コンソールに表示: 20 resolve(number); }); })
.then(treble) // 203 -> 60 .then(dump) // コンソールに表示: 60 .then(double) // 60*2 -> 120 .then(dump); // コンソールに表示: 120
このような仕組みが用意されているため、Promiseを使うことで、同期処理と非同期処理を自然に混ぜて記述することが可能になっています。
※ ちなみに、ここで用意したfunctiondouble
も、treble
も、処理内容的には同期的なものですが、Promiseを介すと、非同期な処理として扱われます。
並列処理
非同期処理を扱う時、一つずつ順番に処理するのではなく、いくつかの処理を並列に走らせたい場合があります。例えば、何回か例に出している、AjaxでAPIにアクセスするfunction、fetchSomething1
〜fetchSomething4
であれば、これらを同時に実行し、すべての処理が終わったタイミングを知りたいというようなケースを考えてみます。そのような並列処理は、Promise.all
を使うことで簡単に実装できます。以下にそのコード例を挙げます。
var promises = [ fetchSomething1(), fetchSomething2(), fetchSomething3(), fetchSomething4() ];Promise.all(promises) .then(function(results) { console.log(results[0]); // fetchSomething1のresolveに渡された値 console.log(results[1]); // fetchSomething2のresolveに渡された値 console.log(results[2]); // fetchSomething3のresolveに渡された値 console.log(results[3]); // fetchSomething4のresolveに渡された値 doSomethingFinally(); }, function(error) { console.log(error); // 最初にrejectに渡された値 });
Promise.all
にPromiseオブジェクトの配列を渡すと、渡された全てのPromiseオブジェクトがfulfilled
になった時、後続するthen
にて登録された処理が実行されます。このとき、各Promiseオブジェクト内でresolve
に渡された値は、それぞれの結果をまとめた配列として受け取ることができます。上記コードだと、then
のonFulfilledハンドラで受け取っているresults
がその配列です。
また、いずれかのPromiseオブジェクトがrejected
になった場合、then
のonRejectedハンドラが実行されます。この時、rejected
にされたPromiseオブジェクト内で、reject
に渡された値を受け取ることができます。
このような処理をPromise.all
なしで書くには、コールバックのように一筋縄で済ませることはできません。各処理が終わったかどうかをフラグを立ててチェックしたりなど、なかなか地味な処理を書く必要がありそうです。しかし、Promiseがあれば簡潔に処理を記述することができます。
エラーをまとめてキャッチ
then
のonRejectedハンドラは、Promiseオブジェクトがrejected
になったときに実行されます。この時、then
にonRejectedハンドラが指定されていない場合、後続するthen
のonRejectedハンドラが実行されます。例えば以下のような例を見てみます。
fetchSomething1() .then(fetchSomething2) // A .then(doSomethingFinally, handleError); // B
今までの例と同様、fetchSomethingX
は、AjaxでAPIにアクセスする、非同期で、Promiseオブジェクトを返すfunctionです。ここで、fetchSomething1
の中でエラーが発生し、fetchSomething1
が返すPromiseオブジェクトがrejected
になったとします。すると、上記コメントAで示したthen
に指定されたonRejectedハンドラが実行されるのですが、AにはonRejectedハンドラが指定されていません。その場合は、続きのthen
に指定されたonRejectedハンドラが実行されます。この結果、上記コメントBにて指定されているonRejectedハンドラhandleError
が実行される結果となります。
これはエラーの発生をまとめて扱いたい際に便利な機能です。同様に順次AjaxでAPIにいくつもアクセスする場合、以下のようにエラー処理をまとめて行うことができます。
fetchSomething1() .then(fetchSomething2) .then(fetchSomething3) .then(fetchSomething4) .then(fetchSomething5) .then(doSomethingFinally, handleError);
上記のように書けば、fetchSomething1
〜fetchSomething5
のどこかでPromiseがrejectされた場合、後続するthen
に指定されたonFulfilledハンドラは実行されず、handleError
が実行されます。
then
の第二引数は省略可能なため、onFulfilledハンドラだけを指定することが可能ですが、このようにエラー処理だけを個別に書きたい場合、例えば以下のように書くことで、onRejectedハンドラだけを指定することができます。
fetchSomething1() .then(fetchSomething2) .then(doSomethingFinally) .then(null, handleError); // 第一引数にnull
上記のように書いてもよいですが、Promiseは、上記と同様の動作をするcatch
というメソッドを用意しています。このような場合、catch
を使い、以下のように書いたほうが簡潔なコードとなるでしょう。
fetchSomething1() .then(fetchSomething2) .then(doSomethingFinally) .catch(handleError);
ライブラリやPolyfill
Promiseは、2015年9月現在、一般的に普及しているブラウザに十分対応されているという状況ではありません(caniuse – Promise)。しかし、ブラウザの対応を待ってからでないと利用できないと類の機能ではありません。Promiseオブジェクトと同様の挙動が実現できればよいわけで、既に様々なライブラリ、Polyfillが存在し、広く使われています。ES6のPromiseが使えない環境であっても、これらライブラリを利用すれば、様々なプロジェクトに活用することができるでしょう。
- petkaantonov/bluebird
- tildeio/rsvp.js
- getify/native-promise-only
- calvinmetcalf/lie
- kriskowal/q
- paulmillr/es6-shim
最後に
以上、2回に渡り、Promiseの機能概要を紹介しました。普段からJavaScriptを使って開発をしている者としては、Promiseっていう機能がECMAScriptに追加されたぞ!と言うより、ついに標準仕様となったか…!と感じる人がほとんどなのではないでしょうか。筆者も、jQueryのDeferredや、AngularJSの$qを日常的に使っていたので、Promiseのインターフェイスには馴染みがあります。これからは、これまで以上に当たり前のようにPromiseが使われるようになっていくでしょうから、この機会に正しく仕様を理解しておきたいところです。
Promiseについてより詳しく知りたい場合、以下を参考にすることをおすすめします。