HTML5Experts.jp

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

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

ひとつずつ処理されるJavaScript

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

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();

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

var result1 = 1 + 2;

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

var result2 = result1 + 100;

が実行され、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ですが、後から任意の処理をさせる方法がもちろんありますし、普段から利用しているでしょう。その方法の一つとして、イベントの利用が挙げられます。

document.getElementById('img1').addEventListener('click', function() {
  alert('画像がクリックされました');
}, false);

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

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

setTimeout(function() {
  alert('5秒経ったみたいです');
}, 5000);

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

コールバック

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

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

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を指定できるようにした……というような実装をしたとすると、例えば以下の様な形になるでしょうか。

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を使って書いてみたサンプルを例とします。

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

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

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

コンストラクタである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に変化させます。

まとめると、以下のようになります。

APIへのアクセス成功可否によりPromiseオブジェクトの状態が変化しますが、このfunction fetchSomething1を実行した側からすれば、ただひとつのPromiseオブジェクトが返ってくるだけです。

then

このようにして作ったPromiseオブジェクトのメソッドを呼ぶことで、状態変化が起こった時に実行されるfunctionを登録することができます。それを行うのがthenです。以下のように使います。

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を使って何が嬉しいのかよく分からないかもしれません。次回は、複数の非同期処理を順次処理する方法、並列に処理する方法、エラーを効率的にハンドリングする方法等を紹介していきます。