Promiseで簡単!JavaScript非同期処理入門【前編】

ECMAScript 2015(ECMAScript 6)で新たに追加されたPromiseについて、その概要を全2回に渡って紹介します。

ひとつずつ処理されるJavaScript

まず、Promiseについて解説する前に、基礎的なことではありますが、JavaScriptのコードがどのようにJavaScriptエンジンに処理されるかについて、軽く解説しておきましょう。例えば以下の様なコードがあったとします。

このコードを実行すると、まず、

が実行され、resultには3が入ります。そして次に、

が実行され、result2には103が入ります。その後、3つのfunctionが準備され、doSomething1()doSomething2()doSomething3()と、準備したfunction群が順番に実行されます。JavaScriptは基本的にシングルスレッドであり、一つの処理が完了するまで次の処理が実行されないようになっています。

2行目のresult1 + 100が実行できるのも、1行目でresult11 + 2の結果が入っているからですし、3つのfunctionが実行できるのも、それよりも前に各functionが準備されたからです。そして、各function内で使用されているresult2には、2行目で計算された103が入っています。当たり前と言えば当たり前ですが。

また、function doSomething2内にはalertがあり、実行されると「計算結果: 103」とダイアログが出現します。ここで、ユーザーがそのダイアログを閉じなければ、処理は先に進みません。ほか、非常に複雑な計算をしたりなどし、一つの処理にとても時間がかかる場合などでも、その処理が終わるまでは、次の処理が始まりません。

非同期な処理

そんなJavaScriptですが、後から任意の処理をさせる方法がもちろんありますし、普段から利用しているでしょう。その方法の一つとして、イベントの利用が挙げられます。

このように、addEventListenerを使えば、img要素がクリックされたタイミングで任意の処理を走らせることができます。

ほか、setTimeoutのような、処理の実行をスケジュールする組み込み関数を利用すれば、後から任意の処理を実行させることができます。例えば、setTimeoutを以下のように使えば、指定したfunctionは5秒後に実行されることになります。

当然、このコードが実行された時、ブラウザが5秒間固まったままになってしまうわけではありません。imgのクリックについても同様、もちろん画像がクリックするまでブラウザが固まってしまうわけではありません。addEventListenersetTimeoutは、functionを受け取り、時が来たらそのfunctionを実行するように作られています。渡したfunctionは、即座に実行されるわけではないため、非同期に処理されるといえます。

コールバック

JavaScripで書かれたプログラムでは、そのような非同期処理を行いたい場合、functionの受け渡しをを利用して実現されてきました。今例に挙げたaddEventListenersetTimeoutも、関数を受け取っている点に注目して下さい。このように、なにかしらの処理が完了したら渡したfunctionを実行させるという実装手法は、「コールバック」と呼ばれています。このコールバックを使って非同期処理を書くというスタイル、単純なケースであればシンプルで分かりやすいのですが、問題もあります。

以下は、決められた順番でアクセスしなければならないAPI4つを順に叩き、その結果を使って何か最終的に処理を行ったという想定の例です。この中で使われているdoAjaxStuffは、XMLHttpRequestを使ってGETなりPOSTなりのリクエストを送る、いわゆるAjaxを行うfunctionであると想像して下さい。リクエストしたいパラメーターなり、リクエストの成功時、失敗時に実行したいfunctionを受け取れるものとします。

それぞれの動作はすぐに終わるものではなく、時間がかかるもの。そしてその処理が終わった時に別の処理を行わせるために今のコールバックを利用するとすると、functionの中にfunction、その中にまたfunction…と、マトリョーシカみたいな入れ子functionを作ることになってしまうことがあります。

コールバックの仕組みを利用する以上、仕方のないことなのですが、複雑な処理の場合、この書き方はなかなかに読みづらいものです。ここにエラー処理を加えた場合、さらにややこしくなります。第二引数にエラー時に実行させたいfunctionを指定できるようにした……というような実装をしたとすると、例えば以下の様な形になるでしょうか。

これだとどこにどのエラー処理を書けばいいのか、なかなかに解読が困難です。

ブラウザ上で動作させるJavaScriptの場合、APIにアクセスしたりなどするためにAjaxを利用することが多いでしょう。複雑なWebアプリケーションの場合、いくつものAPIにアクセスし、返ってきたデータ群からDOMを生成し、ページを作ったりするかもしれません。そんな時、このように複雑なコールバックの入れ子になってしまうことは珍しいことではありません。ほか、複数の非同期処理が全て完了したい時に何か処理をさせたいというケースもよくあります。時代の流れとともにとでも言いましょうか、JavaScriptには、非同期処理をより柔軟に行える機能が求められてきたと言ってしまって間違いはないでしょう。

Promise!

そんな時代に登場した非同期処理の救世主?が、Promiseです。Promiseを使えば、実行と完了に遅延がある処理をうまい具合に扱うことができます。いろいろと説明するよりも、コードを見ながら理解していきましょう。以下がPromiseを使った非同期処理の例です。この例では、先ほど例として挙げた、APIにアクセスするfunction fetchSomething1を、Promiseを使って書いてみたサンプルを例とします。

function fetchSomething1は、Promiseオブジェクトを作り、返します。

ここで何が起こっているかざっと解説してみます。

Promiseオブジェクトの状態

Promiseを理解するためにはまず、Promiseオブジェクトの「状態」について理解することが必要です。

何はともあれ、最初にすることは、Promiseオブジェクトの作成です。Promiseオブジェクトを作るには、コンストラクタであるPromisenewすればよいだけです。このPromiseオブジェクトを経由し、処理がうまくいった場合、いかなかった場合の処理を続けて書くことができます。それをどう書くのかは後述するとして、まずはPromiseオブジェクトの状態についてです。

Promiseオブジェクトには状態があります。それは以下の3種類です。

  • pending: 未解決
  • fulfilled: 無事完了した
  • rejected: 棄却された

最初はpendingになっていますが、無事完了するとfulfilled、棄却されるとrejectedになります。pendingからは、fulfilledrejectedのいずれかの状態に変化することができますが、一度状態が変化したら、それ以上状態を変化させることはできません。一方通行です。

states

コンストラクタであるPromiseには、newする時、functionを引数として渡します。このfunction内にPromiseがラップしたい処理を書きます。

このfunctionには、2つの引数を指定することができます。まずは第一引数として指定しているresolve。これは必ず指定する必要があります。このresolveはfunctionで、処理結果が正常だった場合に実行させます。ここでは、APIへのアクセスに成功した時に実行させています。resolveを実行すると、Promiseオブジェクトの状態がfulfilledになります。「無事完了した」ことにするfunctionです。

第二引数であるrejectもfunctionですが、このrejectresolveとは逆で、処理結果がエラーであった場合に実行させます。ここでは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です。以下のように使います。

thenには二つの引数を渡すことができます。第一引数として渡したfunctionは、Promiseオブジェクトの状態がfulfilledになった時、第二引数として渡したfunctionは、Promiseオブジェクトの状態がrejectedになった時に実行されます。thenの第二引数は省略可能です。

結果として、fetchSomething1のメインの処理が無事完了すれば、「API1よりデータを取得しました」と、棄却されたら、「API1よりデータを取得できませんでした。エラーメッセージ: APIにアクセスできませんでした」とalertが出ます。

このthenに渡したfunctionは、resolve及びrejectが実行された時に渡された値を受け取ることができます。ここでは、reject時に渡されているオブジェクトを受け取り、エラーメッセージとして利用しています。

次回に続く

今回は、JavaScriptにおいて非同期処理を扱う方法と、Promiseのごく基礎的な書き方について紹介しました。今回の内容だけでは、Promiseを使って何が嬉しいのかよく分からないかもしれません。次回は、複数の非同期処理を順次処理する方法、並列に処理する方法、エラーを効率的にハンドリングする方法等を紹介していきます。

'; js_seriesContent.className = "js_seriesContent"; js_seriesContent.innerHTML = js_seriestitle.innerHTML; js_seriesContent.appendChild(js_serieslist_ul); if ( js_parent.lastChild == js_superior ) { js_parent.appendChild(js_seriesContent); } else { js_parent.insertBefore(js_seriesContent, js_superior.nextSibling); } if (js_serieslist_li_length > 5) { document.getElementsByClassName('moveToSeriesTop')[0].style.display = 'block'; document.getElementsByClassName('moveToSeriesTop')[0].href = document.getElementsByClassName('seriesmeta')[0].getElementsByTagName('a')[0].href; } })(this, this.document); // ソーシャルボタンをクリックされたらgaに送信 var elements, i; elements = document.querySelectorAll('.sns-buttons > li > a.facebook-btn-icon-link'); for (i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function() { ga('send', 'social', 'Facebook', 'like', '/takazudo/17107/'); }, false); } elements = document.querySelectorAll('.sns-buttons > li > a.twitter-btn-icon-link'); for (i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function() { ga('send', 'social', 'Twitter', 'tweet', '/takazudo/17107/'); }, false); } elements = document.querySelectorAll('.sns-buttons > li > a.google-plus-btn-icon'); for (i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function() { ga('send', 'social', 'Google+', '+1', '/takazudo/17107/'); }, false); } elements = document.querySelectorAll('.sns-buttons > li > a.hatena-btn-icon'); for (i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function() { ga('send', 'social', 'Hatebu', 'bookmark', '/takazudo/17107/'); }, false); } elements = document.querySelectorAll('.sns-buttons > li > a.pocket-btn-icon'); for (i = 0; i < elements.length; i++) { elements[i].addEventListener('click', function() { ga('send', 'social', 'Pocket', 'bookmark', '/takazudo/17107/'); }, false); }

週間PVランキング

新着記事

Powered byNTT Communications

tag list

アクセシビリティ イベント エンタープライズ デザイン ハイブリッド パフォーマンス ブラウザ プログラミング マークアップ モバイル 海外 高速化 Angular2 AngularJS Chrome Cordova CSS de:code ECMAScript Edge Firefox Google Google I/O 2014 HTML5 Conference 2013 html5j IoT JavaScript Microsoft Node.js Polymer Progressive Web Apps React Safari SkyWay TypeScript UI UX W3C W3C仕様 Webアプリ Web Components WebGL WebRTC WebSocket WebVR