Tech Racho エンジニアの「?」を「!」に。
  • 開発

JSの非同期処理を初めてES6のPromiseを使ったものに書き換えてみた

はじめに

「javascriptの非同期処理を同期ぽく綺麗に書けるようになるPromiseというのがある」
という話は少し小耳にはさんでいて、便利なんだろなーと思いつつも手が出ていませんでした。

意を決してお勉強して、自分の理解を文字に起こしつつ非同期処理をPromiseを使った形式に書き換えていってみたらけっこう分かった気がしましたし、初学者向けの記事になりえるなと思ったので公開します。

PromiseとJavascriptにおけるPromise

まずPromiseとはなんぞやってことなんですが、PromiseはFutureというデザインパターンの別名らしいです。
http://ja.wikipedia.org/wiki/Future

ちょっと上記から引用すると
「future, promise, delay とは、プログラミング言語における並列処理のデザインパターン。何らかの処理を別のスレッドで処理させる際、その処理結果の取得を必要になるところまで後回しにする手法。処理をパイプライン化させる。1977年に考案され、現在ではほとんどのプログラミング言語で利用可能。」

とのことですので、別に新しいものではなく、並列処理で必要なデザインパターンだと。いろんな環境でもともと実装されているけど、javascriptでは実装されていなかったので非標準ライブラリでいろいろ実装されたと。でもってやっぱり便利なので、ECMAScript6=ES6で標準で実装されることになったと。
ということで、標準になるみたいですしなら勉強しときましょうってことですね。

非同期処理をPromiseを使って書き換える

ではさっそくなんとなく書き換えていきましょう。
以下からpromiseの自分の理解をつらつらと書き下しつつ, Promiseを使った処理に書き換えていくときのメモです。よって、ちょっと口調が適当です。あしからず。

ある非同期処理A(webapiの呼び出しなど)を行い、成功時には処理Bを、失敗時には処理Cを実行したいとする。

function A(callback) {
  callWebApi(function (error, data) {
    if (error) {
      callback(error);
    } else {
      callback(null, data);
    }
  });
}
function B(data) { console.log(data) }; // これ以後の参考ソースでは略す
function C(error) { console.log(error) }; // これ以後の参考ソースでは略す

A(function (error, data) {
  if (error) {
    C(error);
  } else {
    B(data);
  }
});

これをpromiseを使って書き換えることを考える。

promiseに非同期処理(もしくは処理に時間がかかるもの)を渡すとpromiseオブジェクトを作れる。promiseに渡した処理の成功時に実行したい処理をpromiseオブジェクトのthenメソッド、失敗時に実行したい処理をcatchメソッドで渡す。catchの代わりにthenの第2引数に失敗時に実行したい処理を渡すことができるのでこちらのほうが便利か。

promiseに渡す非同期処理は処理の結果次第でresolveとrejectを呼ぶ必要がある。resolveとrejectがpromiseオブジェクトに成功、失敗を伝える役目を担う。primiseオブジェクトはresolveとrejectで通知された成功、失敗の状態を保持する。

promiseに渡した非同期処理はすぐに実行される。

promiseに渡した非同期処理はすぐに実行されるため、「非同期処理がすぐに終わったらどうなるの?thenを呼ぶ前に終わったらどうなるの?怖い」と最初は思いがちだが実行結果を保持して待つため問題ない。例の場合だとpromiseにAを渡してにthenでB,Cを渡してやると、thenを呼んだ際にAが終わっていようが終わっていまいが、必ずAが終わった後にBかCが呼び出されることになる。

先のA,B,Cにpromiseを使うには、Aがerrorの有無によりコールバックを引数を変えて呼び分けていたところを、resolveとrejectを呼ぶように書き換える必要がある。そうなると「コールバックでやっていたAからB,Cへのデータの受け渡しはどうするの?」と思うけど、resolveとrejectの引数に入れればそれが素直にthen, catchの処理に渡される。

以上をふまえて書き換えると以下のようになる。

function A(resolve, reject) {
  callWebApi(function (error, data) {
    if (error) {
      reject(error);
    } else {
      resolve(data);
    }
  });
}

p = new Promise(A);
p.then(B, C);

Aの中でpromiseを作るようにするとよりぽくかける。

function A() {
  return new Promise(function (resolve, reject) {
    callWebApi(function (error, data) {
      if (error) {
        reject(error);
      } else {
        resolve(data);
      }
    });
  });
}

A().then(B, C);

これで処理の外側としては綺麗に書けるようになったけど、Aの中がPromiseのせいで余分にネストしている気がする。その対策も用意されていて、DeferredというPromiseとresolveとrejectを持つオブジェクトを利用して書き直せる。deferredの中身は{promise: <Promise>, resolve: <Function>, reject: <Function>}な感じ。

function A() {
  var deferred = Promise.defer();
  callWebApi(function (error, data) {
    if (error) {
      deferred.reject(error);
    } else {
      deferred.resolve(data);
    }
  });
  return deferred.promise;
}

A().then(B, C);

結果

promise使用前と使用後のソースの差は以下。B,Cについては無視してカウント。

使用前
  ネスト 2回
  error 6回
  data 4回
  callback 3回
  if 2回
  function 3回
  () 11個
  {} 7個

使用後
  ネスト 1回
  error 3回
  data 2回
  callback 0回
  if 1回
  function 1回
  () 9個
  {} 4個

だいたい半分になった感じでしょうか。書き方にもよるのでこのデータに意味があるかどうかはちょっとあれで若干ネタですけど、まあ自分としてはpromiseは有用だなぁと思った次第です。

おわりに - 参考情報

最初の理解としてはこんな感じでしたが、さらに便利な実践的な使い方としては、非同期処理が複数回ネストしたのを

A(function (error, result) {
  if (error) {
    console.log(error);
  } else {
    B(result, function (error, result) {
      if (error) {
        console.log(error);
      } else {
        C(result, function (error, result) {
          if (error) {
            console.log(error);
          } else {
            D(result, function (error, result) {
              if (error) {
                console.log(error);
              } else {
                E(result, function (error, result) {
                  if (error) {
                    console.log(error);
                  } else {
                    console.log(result);
                  }
                }); 
              }   
            }); 
          }   
        }); 
      }   
    }); 
  }
});

綺麗に書き直せるとか、

A()
.then(B)
.then(C)
.then(D)
.then(E)
.catch(function (error) {
  console.log(error)
});

複数の非同期処理の終了を待って実行とか、

Promise.all([A(), B(), C()].then(function (results) {
  console.log(results); // [Aの結果, Bの結果, Cの結果]
}));

いろいろあるみたいですが、初心者が最初の理解していく際のプロセスをなぞってみた記事なのでとりあえずここまでにしておきます。
上記の便利な機能らのさらなる理解、ちゃんとした用語などについては以下のサイト様らの記事をご参照ください。

Promiseが実装された
ES6のpromiseの使い方がだいたい分かります。

あなたが読むべきJavaScript Promises
読むべきぽいいろいろな記事まとまっています。

ES6 Promiseの何が美しいのか
自分も勉強したときにいちばん?がでたのが
「PromiseとDeferredてなんなん?なんでおんなじようなのが2つもあるの?」
ってことだったし、いまもPromiseとDeferredの違いとか理解してないので、
もしかするとdefer()は使わないほうがよいお作法なのかもしれません。

Node.jsにPromiseが再びやって来た!
nodeでのpromiseの鮮度の高い情報

"promise" による JavaScript での非同期プログラミング
XMLHttpRequest2を例に使ったまじめな記事。(他の記事がまじめじゃないわけじゃないですが)

Promise
MDNは基本。素振りと精神攻撃も基本。

注:ちなみにソースはひとつも動かしてないので、たぶん動きません^^


CONTACT

TechRachoでは、パートナーシップをご検討いただける方からの
ご連絡をお待ちしております。ぜひお気軽にご意見・ご相談ください。