[Javascript] Promise/A+仕様を、チュートリアル形式で詳しく解説します
こんにちは、@yoheiMuneです。
JavaScriptの実装において、「Promise」を聞いたことがある方も多いのではないでしょうか。 今回は、JavaScriptにおけるPromise/A+を深く学ぶことができる記事を翻訳しました。その内容を紹介したいと思います。
それでは、始まりますー!!
この記事では、Promiseの仕様を少しずつ実装していき、最終的にはPromise/A+の仕様をほぼ満たすように実装します。また、なぜPromiseが非同期プログラミングをサポートする必要があるのか、についても解説します。本記事は、ある程度Promiseを知っている方を想定しています。もしPromiseについて初めて触れるという方は、まずpromisejs.orgをご覧頂くと良いと思います。
この実装はCalbackパターンの単なる置き換えであり、これだけではあまり意味のあるものではありません。しかし、とてもシンプルな実装であるものの、Promiseの根本的な考え方を既に捉えています。
これが、Promiseがすばらしいと感じる最も大きなポイントです。一度、Promise内に処理結果が保持されれば、その後はとても強力に物事を進めることができます。この点については後ほど説明します。
このハックにより、とりあえずはコードが動くようになります。
少し複雑になりましたが、処理を呼び出す側はいつでも好きな時に
これは、
実装すべき仕様は他にもまだまだありますが、この時点で、私たちのPromiseはかなり仕様を実装しています。この実装を利用すれば、
Promiseオブジェクトは、処理結果として立ち振る舞います。Promiseを、必要な時に好きなだけ、他の処理に渡したり、保持したり、利用したりすることができます。
ここで、私たちのPromiseクラスに、メソッドチェーンの概念を追加しましょう。
さて、コードが少し複雑になりました。ここで重要なポイントは、
2番目のPromiseは、何の値を解決するのでしょうか。その値は、1番目のPromiseの返却値です。2番目のPromiseは、1番目の返却値を引数に受け取ります。この処理は、
Promiseをより良く使う方法として、Promiseライブラリの
Promiseを受け取る限り、
さて、
Promiseは
それでは、もう一度Promiseの実装の全体像を確認しましょう。今回は、否認機能が追加されています。
これが意味するところは、
次の例を考えてみましょう。
さて、何が起こるでしょうか。
なぜ、そのエラーコールバックは呼び出されないのでしょうか。これは、JSONのパースに失敗した例外は
もし上記のエラーを捕捉したい場合には、エラーコールバックを、次の
この問題を回避するために、Promiseは常に非同期に解決を行う必要があります。例え、非同期である必要がないとしてもです。Promiseが非同期に解決を行うことで、Promiseの利用者は、Promiseが非同期処理に対応しているか否かを考慮する必要がなくなります。
この記事で実装した内容と実際の実装には、いくつかの相違点が存在します。それは、Promise/A+にはここでは扱っていない更に詳しい仕様が定義されているからです。ぜひ、Promise/A+の仕様書(英語)を読んでみてください。仕様書は、比較的短く、とても読みやすい内容です。
私は、Promiseがどのように動くのか、そして何を解決しようとしているのかを理解してからというもの、Promiseを本当に気に入っています。Promiseは、私のプロジェクトコードをとても簡潔にそして優雅にしてくれます。もっともっと話したいことがあります。この記事は序章でしかありません。
もしこの記事を気に入って頂けたなら、私のTwitter(原作者のTwitterです)をフォローしてください。新しい記事をお知らせします。
今後もすごく良いと思った記事をまた翻訳したいと思いますので、ぜひ私のTwitterやこのブログのRSSもどうぞ宜しくお願いします。
最後までご覧頂きまして、本当にありがとうございました。
JavaScriptの実装において、「Promise」を聞いたことがある方も多いのではないでしょうか。 今回は、JavaScriptにおけるPromise/A+を深く学ぶことができる記事を翻訳しました。その内容を紹介したいと思います。
Special Thanks to https://flic.kr/p/b5bNqv
翻訳について
本記事は、Matt GreerのJavaScript Promises ... In Wicked Detailの翻訳記事です。翻訳するにあたり、ご本人から許可を頂いています。 本記事では、JavaScriptにおけるPromiseの仕様がどっぷりと解説されていますので、お時間を割いて読んで頂く価値があると思います!それでは、始まりますー!!
はじめに
私はJavaScriptを使った開発において、長らくPromiseを使ってきました。Promiseは、最初のうちはとっつきにくいものでした。今となっては、それをとても効果的に使えていますが、Promiseの詳細な部分となると、どのように動作しているのか十分には理解できていません。そんな私自身の問題を解決するために、この記事を書きました。(長いですが)最後まで読むことで、読者の方々もPromiseについて詳しく理解できると思います。この記事では、Promiseの仕様を少しずつ実装していき、最終的にはPromise/A+の仕様をほぼ満たすように実装します。また、なぜPromiseが非同期プログラミングをサポートする必要があるのか、についても解説します。本記事は、ある程度Promiseを知っている方を想定しています。もしPromiseについて初めて触れるという方は、まずpromisejs.orgをご覧頂くと良いと思います。
目次
- なぜPromiseの詳細を扱うのか?
- もっともシンプルな例
- Promiseは状態を持つ
- Promiseにおけるメソッドチェーン
- Promiseを否認する
- Promiseの解決には、非同期が必要です
- then/promiseの話を締めくくる前に
- 最後に
- 更なる学習のために
なぜPromiseの詳細を扱うのか?
どうして、Promiseを詳しく理解する必要があるのでしょうか。それは、Promiseがどのように動作しているのかについて深く理解することで、それを活用する能力が向上するためです。また、何らかの問題が起きた時にも、より効果的にデバッグすることができます。この記事を書くきっかけとなったことは、私と同僚がPromiseの特殊な使い方に悩まされたことでした。その時は大変な思いをしましたが、そのおかげでPromiseについてより深く理解することができました。もっともシンプルな例
まず初めに、最もシンプルなPromiseの実装をしてみましょう。以下の関数があるとします。doSomething(function(value) { console.log('Got a value:' value); });この関数を、次のように使いたいとします。
doSomething().then(function(value) { console.log('Got a value:' value); });これを実現するために、doSomething()の実装を以下のような実装から
function doSomething(callback) { var value = 42; callback(value); }次のような「Promise」を用いた実装に変更します。
function doSomething() { return { then: function(callback) { var value = 42; callback(value); } }; }Fiddle
この実装はCalbackパターンの単なる置き換えであり、これだけではあまり意味のあるものではありません。しかし、とてもシンプルな実装であるものの、Promiseの根本的な考え方を既に捉えています。
“”Promiseは、そのオブジェクト内に処理結果を保持する””
これが、Promiseがすばらしいと感じる最も大きなポイントです。一度、Promise内に処理結果が保持されれば、その後はとても強力に物事を進めることができます。この点については後ほど説明します。
Promiseクラスを定義する
上記の単純なオブジェクトリテラルな実装では、すぐに上手くいかなくなります。ここでは、これからより詳細な説明を行うために、Promiseクラスを定義します。function Promise(fn) { var callback = null; this.then = function(cb) { callback = cb; }; function resolve(value) { callback(value); } fn(resolve); }これを用いて
doSomething()
をリファクタリングすると、次のようになります。function doSomething() { return new Promise(function(resolve) { var value = 42; resolve(value); }); }さて、ここで問題があります。実行してみると分かりますが、
resolve()
がthen()
よりも先に呼び出され、その結果、callback
はnull
となってしまいます。この問題に対応するため、ここではsetTimeout
を使って(ハックとなりますが)対応しましょう。function Promise(fn) { var callback = null; this.then = function(cb) { callback = cb; }; function resolve(value) { // callbackの呼び出しについて // イベントループを1つ待つことで、 // then()が呼び出される機会を作ります setTimeout(function() { callback(value); }, 1); } fn(resolve); }fiddle
このハックにより、とりあえずはコードが動くようになります。
このコードは脆く、そして良くない
このPromiseの実装は脆く、正常に動作させるために、非同期処理を利用する必要があります。この実装でエラーを発生させることは簡単で、then()
を非同期に呼び出すせばよいのです。そうすれば、再びcallback
はnull
になってしまいます。なぜ、こんなにも脆いコードを紹介したのか、と疑問に思うかもしれませんね。それは、この実装はとても理解しやすいものだからです。上記のような簡単な実装であれば、Promiseで大切なthen()
やresolve()
がコードの中ではっきりと分かると思います。then()
やresolve()
は、Promiseにおいて重要な概念です。Promiseは状態を持つ
先ほど紹介した脆いコードは、思いがけなくあることを明らかにしました。それは、Promiseは状態を持つ、ということです。私たちは、処理を開始する前にPromiseがどのような状態であるかを知る必要があり、またその状態がどのように変化するかを正確に把握する必要があります。それでは、コードの脆い部分を取り除きましょう。- Promiseは値が確定するまではpending状態で、値が決まるとresolved状態となります。
- 一度解決された値は、その後は常にその値を保持し、もう一度解決を行うことはありません。
(Promiseにはrejectedという状態もありますが、エラーハンドリングについては後ほど紹介します)
setTimeout
のハックを取り除き、状態の変化を正確に把握できるように、実装を修正します。function Promise(fn) { var state = 'pending'; var value; var deferred; function resolve(newValue) { value = newValue; state = 'resolved'; if(deferred) { handle(deferred); } } function handle(onResolved) { if(state === 'pending') { deferred = onResolved; return; } onResolved(value); } this.then = function(onResolved) { handle(onResolved); }; fn(resolve); }Fiddle
少し複雑になりましたが、処理を呼び出す側はいつでも好きな時に
then()
を呼ぶ事ができるようになりました。また呼び出される側は、任意のタイミングでresolve()
を呼ぶことができるようになりました。同期でも非同期でもどちらにも完全に対応しています。これは、
state
フラグを利用しているためです。then()
もresolve()
も、新しいhandle()
というメソッドに処理を委譲します。handle()
では、状況に応じて2つの処理を使い分けます。- 呼び出される側が
resolve()
を実行する前に、呼び出し側がthen()
を実行した場合は、値がまだ解決されていない状態です。この場合、state
はpending
状態であり、呼び出し側が指定したcallackは、値の解決が行われるまで保持されます。その後、resolve()
が実行されると、callback
は実行され、呼び出し側に値が戻ります。 - 呼び出し側が
then()
を呼び出す前に、呼び出される側がresolve()
を実行した場合、値は確定した状態となります。この場合、then()
が呼び出されればすぐに、値を返却します。
setTimeout
の実装ははなくなってしまいましたが、実は、後ほど復活します。詳細はその時に説明します。
Promiseを用いると、
then()
とresolve()
の呼び出し順序を考慮する必要なくなります。then()
とresolve()
は、利用状況に応じていつでも呼び出す事ができます。この点は、Promiseが処理結果を保持するという機能の、とても大きなメリットの1つです。
実装すべき仕様は他にもまだまだありますが、この時点で、私たちのPromiseはかなり仕様を実装しています。この実装を利用すれば、
then()
を何度呼び出したとしても、常に同じ値を受け取る事ができます。var promise = doSomething(); promise.then(function(value) { console.log('Got a value:', value); }); promise.then(function(value) { console.log('Got the same value again:', value); });
この記事では、Promiseの仕様の全てを正確に実装している訳ではありません。例えば、もし
resolve()
が呼び出される前に複数回then()
を呼び出した場合、最後に呼び出したthen()
のみが有効となります。この問題を解決する1つの方法は、callback
を配列として保持することです。しかし、ここではその実装は行っていません。それは、Promiseの概念を紹介する上で、コードをできるだけシンプルにしたいためです。今までの実装でも、Promiseを説明するために十分に機能します。
Promiseオブジェクトは、処理結果として立ち振る舞います。Promiseを、必要な時に好きなだけ、他の処理に渡したり、保持したり、利用したりすることができます。
Promiseにおけるメソッドチェーン
Promiseはオブジェクト内に値を保持しているため、メソッドチェーンをしたり、map処理をしたり、並列したり、直列したり、と様々な使い方をすることができます。次のコードは、良くあるPromiseの利用例です。getSomeData() .then(filterTheData) .then(processTheData) .then(displayTheData);
getSomeData()
は、直後にthen()
が呼ばれていることからも分かるとおり、Promiseを返却しています。そして、1つ目のthen()
の結果もPromiseであり、その返却値を使って次のthen()
を呼び出しています(その次もしかりです)。then()
でPromiseを返却することで、メソッドチェーンを行っているのです。then()
は必ずPromiseを返却するここで、私たちのPromiseクラスに、メソッドチェーンの概念を追加しましょう。
function Promise(fn) { var state = 'pending'; var value; var deferred = null; function resolve(newValue) { value = newValue; state = 'resolved'; if(deferred) { handle(deferred); } } function handle(handler) { if(state === 'pending') { deferred = handler; return; } if(!handler.onResolved) { handler.resolve(value); return; } var ret = handler.onResolved(value); handler.resolve(ret); } this.then = function(onResolved) { return new Promise(function(resolve) { handle({ onResolved: onResolved, resolve: resolve }); }); }; fn(resolve); }Fiddle
さて、コードが少し複雑になりました。ここで重要なポイントは、
then()
が新しいPromiseを返すということです。then()
が常に新しいPromiseオブジェクトを返すため、一連の処理において、1つ以上の複数のPromiseが存在する場合があります。これは、一見オブジェクトの無駄遣いではないかと思うかもしれません。Callbackの手法では、このような問題は起こりません。これは、Promiseが批判を受ける主要なポイントです。しかし、JavaScriptを扱ういくつかのコミュニティでは、この点からPromiseを敬遠するようなことはやめて、正しく評価しようという取り組み始まっています。
2番目のPromiseは、何の値を解決するのでしょうか。その値は、1番目のPromiseの返却値です。2番目のPromiseは、1番目の返却値を引数に受け取ります。この処理は、
handle()
内の後半部分に実装されており、引数のhandler
には、resolve()
への参照とonResolved
への参照を保持しています。そこにはresolve()
のコピーが存在し、各Promiseは自分自身のresolve()
のコピーや内部で実行するクロージャーを保持します。これが、1つ目のPromiseと2つ目のPromiseの架け橋となります。1つ目のPromiseを次のコードで解決しています。var ret = handler.onResolved(value);この例では、
handler.onResolved
は次の通りです。function(value) { console.log("Got a value:", value); }違う見方をすれば、これが最初の呼び出しとして
then()
に渡される処理です。1番目のhandler
の返却値が、2番目のPromiseを解決するために利用されます。これで、メソッドチェーンの実装は完成です。doSomething().then(function(result) { console.log('first result', result); return 88; }).then(function(secondResult) { console.log('second result', secondResult); }); // アウトプットは、 // first result 42 // second result 88 doSomething().then(function(result) { console.log('first result', result); // 何もreturnしていない }).then(function(secondResult) { console.log('second result', secondResult); }); // アウトプットは、 // first result 42 // second result undefined
then()
は常に新しいPromiseオブジェクトを返すため、好きなだけメソッドチェーンをつなげる事ができます。doSomething().then(function(result) { console.log('first result', result); return 88; }).then(function(secondResult) { console.log('second result', secondResult); return 99; }).then(function(thirdResult) { console.log('third result', thirdResult); return 200; }).then(function(fourthResult) { // これ以降も続けることができます });もし上記のコードで、最後に全ての処理結果を受け取りたい場合にはどうしたら良いでしょうか。メソッドチェーンを使って、必要に応じて独自に結果の受け渡しをすれば良いのです。
doSomething().then(function(result) { var results = [result]; results.push(88); return results; }).then(function(results) { results.push(99); return results; }).then(function(results) { console.log(results.join(', '); }); // アウトプットは、 // [42, 88, 99]
Promiseは常に1つの値の解決のみを行います。もし2つ以上の値を一緒に渡したい場合、複数の値を扱える方法(配列、オブジェクト型、結合した文字列など)を利用する必要があります。
Promiseをより良く使う方法として、Promiseライブラリの
all()
メソッドや、その他Promiseのメリットを引き出す何らかのUtilityメソッドを使うと良いでしょう。何を使うかについては、読者の皆さんの好みに合わせて選択してください。任意のCallback
then()
に指定するcallbackは、厳密に言えば必須ではありません。もしcallback
を省略した場合、Promiseは1つ前のPromiseと同じ内容を解決します。doSomething().then().then(function(result) { console.log('got a result', result); }); // アウトプットは、 // got a result 42既に実装済みの
handle()
の内容を確認すると、callack
がない場合には、Promiseを解決して、処理を終了するようになっていることが分かります。if(!handler.onResolved) { handler.resolve(value); return; }
メソッドチェーンの内部でPromiseを返却する
私たちのメソッドチェーンの実装には、少し繊細さが欠けるところがあります。私たちの実装では、受け取った値について何もチェックせずに、解決済みの値としてそのまま次の処理に渡しています。もし、受け取った値のの1つがPromiseオブジェクトだったらどうなるでしょうか。例えば、次のような場合です。doSomething().then(function(result) { // doSomethingElse returns a Promise return doSomethingElse(result) }).then(function(finalResult) { console.log("the final result is", finalResult); });このコードは、私たちが想定している通りに動きません。
finalResult
には、解決済みの値ではなく、Promiseオブジェクトが渡されます。この実装を意図通りに動作させるために、以下のように実装を修正してみましょう。doSomething().then(function(result) { // doSomethingElse returns a Promise return doSomethingElse(result) }).then(function(anotherPromise) { anotherPromise.then(function(finalResult) { console.log("the final result is", finalResult); }); });かなり煩雑な実装となりました。この解決策として、Promiseの実装を変更して、解決済みの値がPromiseオブジェクトなのか否かを呼び出し側が意識せずに扱えるようにしましょう。これは簡単に実装することができます。
resolve()
に渡された値がPromiseオブジェクトだった場合に、特別な処理を行うだけです。function resolve(newValue) { if(newValue && typeof newValue.then === 'function') { newValue.then(resolve); return; } state = 'resolved'; value = newValue; if(deferred) { handle(deferred); } }Fiddle
Promiseを受け取る限り、
resolve()
を再帰的に呼び出し続けます。引数がPromiseでないものが見つかれば、その時点で処理を次に進めます。
この実装は、無限ループとなる可能性があります。Promise/A+の仕様では、必須ではありませんが、無限ループに対処する実装が推奨されています。
また、ここで紹介している実装は、仕様を完全には満たしていません。同様に、この記事で紹介している実装も、全ての仕様を満たしている訳ではありません。この点についてより詳しく知りたい場合には、仕様のPromise resolution procedureを読むことで、この記事では省いてしまった点も含めて、より正確に理解することができるでしょう。
さて、
newValue
がPromiseオブジェクトかどうかの判定が、かなりいい加減だと思ったのではないでしょうか。then()
メソッドがあるかどうかだけをチェックしています。このダックタイピングは意図的なものです。わざと曖昧に判定することで、Promiseの実装が少し異なるライブラリ同士を組み合わせた場合にも、お互いにPromiseだと解釈することができるようになります。複数のPromiseライブラリを混ぜ合わせることは、実際にはよくあることであり、それぞれのライブラリの実装が少し異なることも良くあることです。
異なるPromiseの実装でも、Promiseの仕様に適切に従っていれば、お互いに解釈することができます。
Promiseを否認する
Promiseを扱っている時に何か問題が生じた場合は、理由とともにreject(=否認)する必要があります。呼び出し元は、否認された場合にどのようにそれを知れば良いのでしょうか。それは、then()
の2つ目の引数にエラー時の処理を渡すことで、エラー通知を受けることができるようにします。doSomething().then(function(value) { console.log('Success!', value); }, function(error) { console.log('Uh oh', error); });
以前に言及した通り、Promiseの状態は、pendingからresolvedかrejectedのどちらかの状態に遷移します。両方の状態になることはありません。言い換えれば、then()の2つのコールバックのうち、どちらか1つのみが呼び出されるということです。
Promiseは
reject()
を実行することでrejected状態にすることができ、reject()
はresolve()
と同じように重要な機能です。それでは、doSomething()
にエラーハンドリングを追加しましょう。function doSomething() { return new Promise(function(resolve, reject) { var result = somehowGetTheValue(); if(result.error) { reject(result.error); } else { resolve(result.value); } }); }Promiseクラスの中に否認機能を実装します。Promiseの仕様では、一度否認されれば、それ以降のすべてのPromiseも否認される必要があります。
それでは、もう一度Promiseの実装の全体像を確認しましょう。今回は、否認機能が追加されています。
function Promise(fn) { var state = 'pending'; var value; var deferred = null; function resolve(newValue) { if(newValue && typeof newValue.then === 'function') { newValue.then(resolve, reject); return; } state = 'resolved'; value = newValue; if(deferred) { handle(deferred); } } function reject(reason) { state = 'rejected'; value = reason; if(deferred) { handle(deferred); } } function handle(handler) { if(state === 'pending') { deferred = handler; return; } var handlerCallback; if(state === 'resolved') { handlerCallback = handler.onRejected; } else { handlerCallback = handler.onResolved; } if(!handlerCallback) { if(state === 'resolved') { handler.resolve(value); } else { handler.reject(value); } return; } var ret = handlerCallback(value); handler.resolve(ret); } this.then = function(onResolved, onRejected) { return new Promise(function(resolve, reject) { handle({ onResolved: onResolved, onRejected: onRejected, resolve: resolve, reject: reject }); }); }; fn(resolve, reject); }Fiddle
reject()
を追加する以外にも、handle()
でも否認を扱うようになりました。handle()
内において、Promiseが否認されるか解決されるかは、state
の値によって決まります。このstate
は次のPromiseオブジェクトにも渡され、そのPromiseでは受け取ったstate
の値を元に、否認または解決を行い、また自分自身のstate
の値に受け取ったstateの値を設定します。
Promiseを使う際に、エラーコールバックを省略することができます。しかし、エラーコールバックを省略した場合には、何か問題が発生した際に、それを知る術がありません。少なくとも、チェーンしている最後のPromiseには、エラーコールバックを指定するべきです。後ほど、この点についてより詳しく説明します。
予期せぬエラーも否認につなげる必要がある
今までのところ、エラーハンドリングは、把握可能なエラーのみを対象にしていました。しかし、想定していないエラーが発生する可能性もあり、この場合にも正しく対処する必要があります。これは極めて重要なことであり、Promiseの実装では、想定外の例外を補足し、適切にrejectすることが求められます。これが意味するところは、
resolve()
がtry/cach
ブロックで囲われるべきだ、ということです。function resolve(newValue) { try { // ... as before } catch(e) { reject(e); } }また同じように重要な点として、呼び出し元が指定したコールバックが、予期せぬ例外を投げるかもしれないということです。これらのコールバックは、
handle()
内で呼び出されます。次のように対処します。function handle(deferred) { // ... as before var ret; try { ret = handlerCallback(value); } catch(e) { handler.reject(e); return; } handler.resolve(ret); }
Promiseはエラーを握りつぶす可能性がある!
Promiseに対する理解が不十分だと、エラーを完全に握りつぶしてしまう実装をしてしまう可能性があります。これは、Promiseの利用者がよくつまづくポイントです。
次の例を考えてみましょう。
function getSomeJson() { return new Promise(function(resolve, reject) { var badJson = "Fiddleuh oh, this is not JSON at all!"; resolve(badJson); }); } getSomeJson().then(function(json) { var obj = JSON.parse(json); console.log(obj); }, function(error) { console.log('uh oh', error); });
さて、何が起こるでしょうか。
then()
の引数のコールバックは、正しい形式のJSONを受け取ることを期待しています。そのコールバックでは、受け取った値をチェックすることなくJSONのパースを試みるため、そこで例外が発生してしまいます。これを実装した人は、何かエラーが合った場合に備えて、then()
の第2引数にエラーコールバックを指定しています。果たして、実装者の意図通りに、エラーコールバックが呼び出されるのでしょうか。
答えはNoです。そのエラーコールバックは呼び出されません!上に掲載したfiddleの例を実行してみれば、何もアウトプットを得られないことが分かります。残念な結果ですね。
なぜ、そのエラーコールバックは呼び出されないのでしょうか。これは、JSONのパースに失敗した例外は
handle()
内でキャッチされますが、JSONのパース処理が呼び出される時には、対象のPromiseは既にresolved状態であるため、rejectは呼び出されないのです。その例外が起きた場合には、次のPromiseが否認されます。
常に意識しておきたいこととして、
then()
のコールバックの中では、Promiseは既にresolved状態である、ということです。コールバックの結果が何であれ、Promiseに影響を与えることはありません。
もし上記のエラーを捕捉したい場合には、エラーコールバックを、次の
then()
で指定する必要があります。getSomeJson().then(function(json) { var obj = JSON.parse(json); console.log(obj); }).then(null, function(error) { console.log("an error occured: ", error); });これで、エラーを適切にログ出力できるようになります。
私の経験上、この点がPromiseにおける最も大きな落とし穴です。次の章を読むことで、より良い解決策を理解することができます。
done()を救済として使う
(全てではありませんが)ほとんどのPromiseのライブラリには、done()
メソッドが存在します。これはthen()
にとても似ていますが、done()
を使うことでthen()
の落とし穴(前章を参照)を回避することができます。done()
は、then()
が記述できるところではいつでも呼び出すことができます。最も大きな違いは、done()
はPromiseを返却しないことです。また、done()
内で発生したいかなる例外もPromiseでは処理されません。言い換えれば、done()
はPromiseのチェーンが全て終わった時に呼び出すということです。getSomeJson()
の例でdone()
を使うと、次のように書き直すことができます。getSomeJson().done(function(json) { // ここで発生した例外は、握りつぶされることはありません。 var obj = JSON.parse(json); console.log(obj); });
done()
にもエラーコールバックを指定することができ、then()
と同じく、done(callback, errback)
といった具合に指定できます。そのエラーコールバック(errback)は、Promiseの処理が完全に終了してから呼び出されるので、Promiseを用いた一連の処理で発生したいかなるエラーも、通知を受けることができます。done()
は(少なくとも今のところは)Promise/A+の仕様ではないため、Promiseのライブラリによっては、実装されていない場合もあります。
Promiseの解決には、非同期が必要です
この記事の最初では、setTimeout
を使うことで、ちょっとしたごまかし実装を行いました。そしてその後、そのハックを修正してsetTimeout
を使わなくなりました。しかし実際のところPromise/A+の仕様では、Promiseの解決は非同期で行われる、ということが想定されています。この仕様を満たすために、handle()
の実装の大半をsetTimeout
の呼び出しで囲う必要があります。function handle(handler) { if(state === 'pending') { deferred = handler; return; } setTimeout(function() { // handle内の大半の処理 }, 1); }Promise/A+で必要な実装は、以上で全てです。実際には、多くのPromiseのライブラリにおいて、非同期をサポートするために
setTimeout
を使っていません。もしそのライブラリがNodeJS上で動作するのであれば、process.nextTick
を使うでしょうし、もしブラウザ上で動作するのであれば、setImmediate
やsetImmediate shim(今のところIEのみ動作します)や、Kris Kowalのasapといったライブラリを使うかもしれません(Kris Kowalは、Qという人気のあるPromiseライブラリを作成しています)。なぜこのような非同期の要件が仕様にあるのか?
非同期をサポートすることで、多くの呼び出し方に対応することができます。以下の奇妙な例を見てみましょう。var promise = doAnOperation(); invokeSomething(); promise.then(wrapItAllUp); invokeSomethingElse();この実装において、処理順はどのようになるでしょうか。関数名から推測すると、
invokeSomething() -> invokeSomethingElse() -> wrapItAllUp()
の順で呼び出されることが想定されているようです。しかし、想定通りの実行順となるかどうかは、Promiseの解決処理が、同期で行われるか、非同期で行われるかによって変わります。もしdoAnOperation()
内で処理が非同期で行われるのであれば、想定通りの実行順となるでしょう。しかし、doAnOperation()
が同期処理の場合は、処理の順序はinvokeSomething() -> wrapItAllUp() -> invokeSomethingElse()
となり、想定とは異なる結果となってしまいます。この問題を回避するために、Promiseは常に非同期に解決を行う必要があります。例え、非同期である必要がないとしてもです。Promiseが非同期に解決を行うことで、Promiseの利用者は、Promiseが非同期処理に対応しているか否かを考慮する必要がなくなります。
Promiseは常に、イベントループ(処理のメインスレッドにおけるループ)が1回以上経過した後に、解決を行うことが求められます。この仕様があるおかげで、コールバック手法が不要となります。
then/promiseの話を締めくくる前に
世の中には、Promiseの仕様を全て満たしたライブラリが数多く存在します。実装方法には様々な方法がありますが、その中でthenチームのpromiseライブラリの実装は、比較的にこの記事と近い実装です。そのライブラリは、シンプルな実装で仕様を満たしており、また仕様以外のこと実装していません。もしその実装内容(@github)を見る機会があれば、その実装がこの記事ととても良く似ていることがわかるでしょう。thenチームのpromiseライブラリは、この記事を書くためのベースとなっており、私たちはここまでの実装で、ほぼ同様の実装をこの記事内で行いました。開発者であるNathan ZadoksとForbes LindsayのJavaScriptにおけるすばらしい活躍に、感謝したいと思います。また、Forbes Lindsayは、promisejs.orgのサイトを立ち上げた人でもあります。この記事で実装した内容と実際の実装には、いくつかの相違点が存在します。それは、Promise/A+にはここでは扱っていない更に詳しい仕様が定義されているからです。ぜひ、Promise/A+の仕様書(英語)を読んでみてください。仕様書は、比較的短く、とても読みやすい内容です。
最後に
最後まで、ご覧頂きましてありがとうございました。ここまで、Promiseの中核となる部分を扱ってきました。多くのPromiseを実装したライブラリでは、all()
, race()
, denodeify()
など他にも様々な機能が提供されています。それらも含めたPromiseの可能性を知るには、API docs for Bluebirdを読むことをお勧めします。私は、Promiseがどのように動くのか、そして何を解決しようとしているのかを理解してからというもの、Promiseを本当に気に入っています。Promiseは、私のプロジェクトコードをとても簡潔にそして優雅にしてくれます。もっともっと話したいことがあります。この記事は序章でしかありません。
もしこの記事を気に入って頂けたなら、私のTwitter(原作者のTwitterです)をフォローしてください。新しい記事をお知らせします。
更なる学習のために
より詳しくPromiseについて学ぶために役立つ情報を紹介します。- promisejs.org — 本文内でも何度か言及しましたが、Promiseに関するとても良いチュートリアルです。
- Q’s Design Rationale — この記事と同じくPromiseを扱っています。本記事よりも詳細な内容が説明されています。
- Some debate over whether done() is a good thing — Github上のIssueの1つですが、done()について深く議論されています。
- Flattening Promise Chains — Thomas BurlesonによるPromiseの記事です。Promiseについて、とても詳しく説明しています。私の記事が「Promiseは何か」を説明しているとしたら、彼の記事は「Promiseのなぜ」を説明しています。
間違いを見つけたら?
もし記事内に間違いがありましたら、メールまたはGithubのIssueとして教えてください。翻訳を終えて最後に
非常に長い記事でしたが、ここまでご覧頂いた方は、Promise/A+の内容を理解できたのではないでしょうか。 私自身、Promiseは聞いたことある、thenとかdoneとかいうメソッドを聞いたことあるというレベルでしたが、この記事を読むことで、Promise/A+の仕様の多くの部分を理解することが出来ました。 この記事を公開してくれて、さらに翻訳も快く許可してくれた、Matt Greerには本当に感謝です。今後もすごく良いと思った記事をまた翻訳したいと思いますので、ぜひ私のTwitterやこのブログのRSSもどうぞ宜しくお願いします。
最後までご覧頂きまして、本当にありがとうございました。