ECMAScript 2015(ECMAScript 6)で新たに追加されたPromiseについて、その概要を全2回に渡って紹介します。
ひとつずつ処理されるJavaScript
まず、Promiseについて解説する前に、基礎的なことではありますが、JavaScriptのコードがどのようにJavaScriptエンジンに処理されるかについて、軽く解説しておきましょう。例えば以下の様なコードがあったとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
var result1 = 1 + 2; // 3 var result2 = result1 + 100; // 103 /* functionらを準備 */ var doSomething1 = function() { document.getElementById('price').value = result2; }; var doSomething2 = function() { alert('計算結果: ' + result2); }; var doSomething3 = function() { console.log('計算結果: ' + result2); }; /* 実行 */ doSomething1(); doSomething2(); doSomething3(); |
このコードを実行すると、まず、
1 |
var result1 = 1 + 2; |
が実行され、result
には3
が入ります。そして次に、
1 |
var result2 = result1 + 100; |
が実行され、result2
には103
が入ります。その後、3つのfunctionが準備され、doSomething1()
、doSomething2()
、doSomething3()
と、準備したfunction群が順番に実行されます。JavaScriptは基本的にシングルスレッドであり、一つの処理が完了するまで次の処理が実行されないようになっています。
2行目のresult1 + 100
が実行できるのも、1行目でresult1
に1 + 2
の結果が入っているからですし、3つのfunctionが実行できるのも、それよりも前に各functionが準備されたからです。そして、各function内で使用されているresult2
には、2行目で計算された103
が入っています。当たり前と言えば当たり前ですが。
また、function doSomething2
内にはalert
があり、実行されると「計算結果: 103」とダイアログが出現します。ここで、ユーザーがそのダイアログを閉じなければ、処理は先に進みません。ほか、非常に複雑な計算をしたりなどし、一つの処理にとても時間がかかる場合などでも、その処理が終わるまでは、次の処理が始まりません。
非同期な処理
そんなJavaScriptですが、後から任意の処理をさせる方法がもちろんありますし、普段から利用しているでしょう。その方法の一つとして、イベントの利用が挙げられます。
1 2 3 |
document.getElementById('img1').addEventListener('click', function() { alert('画像がクリックされました'); }, false); |
このように、addEventListener
を使えば、img要素がクリックされたタイミングで任意の処理を走らせることができます。
ほか、setTimeout
のような、処理の実行をスケジュールする組み込み関数を利用すれば、後から任意の処理を実行させることができます。例えば、setTimeout
を以下のように使えば、指定したfunctionは5秒後に実行されることになります。
1 2 3 |
setTimeout(function() { alert('5秒経ったみたいです'); }, 5000); |
当然、このコードが実行された時、ブラウザが5秒間固まったままになってしまうわけではありません。imgのクリックについても同様、もちろん画像がクリックするまでブラウザが固まってしまうわけではありません。addEventListener
やsetTimeout
は、functionを受け取り、時が来たらそのfunctionを実行するように作られています。渡したfunctionは、即座に実行されるわけではないため、非同期に処理されるといえます。
コールバック
JavaScripで書かれたプログラムでは、そのような非同期処理を行いたい場合、functionの受け渡しをを利用して実現されてきました。今例に挙げたaddEventListener
もsetTimeout
も、関数を受け取っている点に注目して下さい。このように、なにかしらの処理が完了したら渡したfunctionを実行させるという実装手法は、「コールバック」と呼ばれています。このコールバックを使って非同期処理を書くというスタイル、単純なケースであればシンプルで分かりやすいのですが、問題もあります。
以下は、決められた順番でアクセスしなければならないAPI4つを順に叩き、その結果を使って何か最終的に処理を行ったという想定の例です。この中で使われているdoAjaxStuff
は、XMLHttpRequest
を使ってGETなりPOSTなりのリクエストを送る、いわゆるAjaxを行うfunctionであると想像して下さい。リクエストしたいパラメーターなり、リクエストの成功時、失敗時に実行したいfunctionを受け取れるものとします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
var fetchSomething1 = function(done) { // API1にアクセス doAjaxStuff(someOptions, { success: function(data) { done(); // 成功したら渡されたfunctionを実行 } }); }; // fetchSomething1と同じようにそれぞれ別のAPIにアクセスするfunction群 var fetchSomething2 = function(done) { /* 省略 */ }; var fetchSomething3 = function(done) { /* 省略 */ }; var fetchSomething4 = function(done) { /* 省略 */ }; var doSomethingFinally = function() { // APIにアクセスして取得してきたデータを使って何かする }; /* 順番に実行 */ fetchSomething1(function() { fetchSomething2(function() { fetchSomething3(function() { fetchSomething4(doSomethingFinally); // 全部終わったら doSomethingFinally() }); }); }); |
それぞれの動作はすぐに終わるものではなく、時間がかかるもの。そしてその処理が終わった時に別の処理を行わせるために今のコールバックを利用するとすると、functionの中にfunction、その中にまたfunction…と、マトリョーシカみたいな入れ子functionを作ることになってしまうことがあります。
コールバックの仕組みを利用する以上、仕方のないことなのですが、複雑な処理の場合、この書き方はなかなかに読みづらいものです。ここにエラー処理を加えた場合、さらにややこしくなります。第二引数にエラー時に実行させたいfunctionを指定できるようにした……というような実装をしたとすると、例えば以下の様な形になるでしょうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
fetchSomething1(function() { fetchSomething2(function() { fetchSomething3(function() { fetchSomething4(doSomethingFinally, function() { // fetchSomething4のエラー処理 }); }, function() { // fetchSomething3のエラー処理 }); }, function() { // fetchSomething2のエラー処理 }); }, function() { // fetchSomething1のエラー処理 }); |
これだとどこにどのエラー処理を書けばいいのか、なかなかに解読が困難です。
ブラウザ上で動作させるJavaScriptの場合、APIにアクセスしたりなどするためにAjaxを利用することが多いでしょう。複雑なWebアプリケーションの場合、いくつものAPIにアクセスし、返ってきたデータ群からDOMを生成し、ページを作ったりするかもしれません。そんな時、このように複雑なコールバックの入れ子になってしまうことは珍しいことではありません。ほか、複数の非同期処理が全て完了したい時に何か処理をさせたいというケースもよくあります。時代の流れとともにとでも言いましょうか、JavaScriptには、非同期処理をより柔軟に行える機能が求められてきたと言ってしまって間違いはないでしょう。
Promise!
そんな時代に登場した非同期処理の救世主?が、Promiseです。Promiseを使えば、実行と完了に遅延がある処理をうまい具合に扱うことができます。いろいろと説明するよりも、コードを見ながら理解していきましょう。以下がPromiseを使った非同期処理の例です。この例では、先ほど例として挙げた、APIにアクセスするfunction fetchSomething1
を、Promiseを使って書いてみたサンプルを例とします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
var fetchSomething1 = function() { return new Promise(function(resolve, reject) { // API1にアクセス doAjaxStuff(someOptions, { success: function(data) { // 成功した場合 resolve(); }, fail: function() { // 何かしらエラーが発生した場合 reject({ message: 'APIにアクセスできませんでした' }); } }); }); }; fetchSomething1().then(function() { alert('API1よりデータを取得しました'); }, function(error) { alert('API1よりデータを取得できませんでした。エラーメッセージ: ' + error.message); }); |
function fetchSomething1
は、Promiseオブジェクトを作り、返します。
ここで何が起こっているかざっと解説してみます。
Promiseオブジェクトの状態
Promiseを理解するためにはまず、Promiseオブジェクトの「状態」について理解することが必要です。
何はともあれ、最初にすることは、Promiseオブジェクトの作成です。Promiseオブジェクトを作るには、コンストラクタであるPromise
をnew
すればよいだけです。このPromiseオブジェクトを経由し、処理がうまくいった場合、いかなかった場合の処理を続けて書くことができます。それをどう書くのかは後述するとして、まずはPromiseオブジェクトの状態についてです。
Promiseオブジェクトには状態があります。それは以下の3種類です。
pending
: 未解決fulfilled
: 無事完了したrejected
: 棄却された
最初はpending
になっていますが、無事完了するとfulfilled
、棄却されるとrejected
になります。pending
からは、fulfilled
、rejected
のいずれかの状態に変化することができますが、一度状態が変化したら、それ以上状態を変化させることはできません。一方通行です。
コンストラクタであるPromise
には、new
する時、functionを引数として渡します。このfunction内にPromiseがラップしたい処理を書きます。
このfunctionには、2つの引数を指定することができます。まずは第一引数として指定しているresolve
。これは必ず指定する必要があります。このresolve
はfunctionで、処理結果が正常だった場合に実行させます。ここでは、APIへのアクセスに成功した時に実行させています。resolve
を実行すると、Promiseオブジェクトの状態がfulfilled
になります。「無事完了した」ことにするfunctionです。
第二引数であるreject
もfunctionですが、このreject
はresolve
とは逆で、処理結果がエラーであった場合に実行させます。ここではAPIへのアクセスが失敗に終わった時に実行させています。reject
を実行すると、Promiseオブジェクトの状態がrejected
になります。「棄却された」ことにするfunctionです。この第二引数は省略可能です。
このようにして、Promiseコンストラクタに渡したfunction内で、Promsieオブジェクトの状態をpending
からfulfilled
及びrejected
に変化させます。
まとめると、以下のようになります。
- function
fetchSomething1
を実行すると、Promiseオブジェクトが返ってくる - APIへのアクセスに成功するとPromiseオブジェクトの状態が
pending
からfulfilled
になる - APIへのアクセスに失敗するとPromiseオブジェクトの状態が
pending
からrejected
になる
APIへのアクセス成功可否によりPromiseオブジェクトの状態が変化しますが、このfunction fetchSomething1
を実行した側からすれば、ただひとつのPromiseオブジェクトが返ってくるだけです。
then
このようにして作ったPromiseオブジェクトのメソッドを呼ぶことで、状態変化が起こった時に実行されるfunctionを登録することができます。それを行うのがthen
です。以下のように使います。
1 2 3 4 5 |
fetchSomething1().then(function() { alert('API1よりデータを取得しました'); }, function(error) { alert('API1よりデータを取得できませんでした。エラーメッセージ: ' + error.message); }); |
then
には二つの引数を渡すことができます。第一引数として渡したfunctionは、Promiseオブジェクトの状態がfulfilled
になった時、第二引数として渡したfunctionは、Promiseオブジェクトの状態がrejected
になった時に実行されます。then
の第二引数は省略可能です。
結果として、fetchSomething1
のメインの処理が無事完了すれば、「API1よりデータを取得しました」と、棄却されたら、「API1よりデータを取得できませんでした。エラーメッセージ: APIにアクセスできませんでした」とalertが出ます。
このthen
に渡したfunctionは、resolve
及びreject
が実行された時に渡された値を受け取ることができます。ここでは、reject
時に渡されているオブジェクトを受け取り、エラーメッセージとして利用しています。
次回に続く
今回は、JavaScriptにおいて非同期処理を扱う方法と、Promiseのごく基礎的な書き方について紹介しました。今回の内容だけでは、Promiseを使って何が嬉しいのかよく分からないかもしれません。次回は、複数の非同期処理を順次処理する方法、並列に処理する方法、エラーを効率的にハンドリングする方法等を紹介していきます。