HTML5Experts.jp

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

前回に引き続き、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よりも、こちらのほうがコードの見通しがよいのではないでしょうか。

fetchSomething1fetchSomething4はそれぞれ、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になった時に初めて実行されます。fetchSomething2fetchSomething1の返すPromiseがfulfilledになった時に実行され、fetchSomething3fetchSometing2の返す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) // 20
3 -> 60 .then(dump) // コンソールに表示: 60 .then(double) // 60*2 -> 120 .then(dump); // コンソールに表示: 120

このような仕組みが用意されているため、Promiseを使うことで、同期処理と非同期処理を自然に混ぜて記述することが可能になっています。

※ ちなみに、ここで用意したfunctiondoubleも、trebleも、処理内容的には同期的なものですが、Promiseを介すと、非同期な処理として扱われます。

並列処理

非同期処理を扱う時、一つずつ順番に処理するのではなく、いくつかの処理を並列に走らせたい場合があります。例えば、何回か例に出している、AjaxでAPIにアクセスするfunction、fetchSomething1fetchSomething4であれば、これらを同時に実行し、すべての処理が終わったタイミングを知りたいというようなケースを考えてみます。そのような並列処理は、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);

上記のように書けば、fetchSomething1fetchSomething5のどこかで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が使えない環境であっても、これらライブラリを利用すれば、様々なプロジェクトに活用することができるでしょう。

最後に

以上、2回に渡り、Promiseの機能概要を紹介しました。普段からJavaScriptを使って開発をしている者としては、Promiseっていう機能がECMAScriptに追加されたぞ!と言うより、ついに標準仕様となったか…!と感じる人がほとんどなのではないでしょうか。筆者も、jQueryのDeferredや、AngularJSの$qを日常的に使っていたので、Promiseのインターフェイスには馴染みがあります。これからは、これまで以上に当たり前のようにPromiseが使われるようになっていくでしょうから、この機会に正しく仕様を理解しておきたいところです。

Promiseについてより詳しく知りたい場合、以下を参考にすることをおすすめします。