今更だけどPromise入門

  • 746
    Like
  • 1
    Comment
More than 1 year has passed since last update.

今年のはじめの方からPromiseの話題は耳にしていたけど
結局よくわかってなかったのでここでPromiseのAPIを理解しておこうと思います。

Promiseとは

非同期の処理をいい感じに使えるAPIパターンです。

Promiseを使ってない場合だと非同期のメソッドを繋げる場合
いわゆるコールバック地獄となってしまいます。

//Promiseを使わない非同期を繋げる場合
A(function(a){
  B(a, function(b){
    C(b, function(c){
      done(c); // ABC
    });
  });
});

でもPromiseを使えばメソッドチェーンにすることができコールバック地獄を回避することができます。

A().then(B).then(C).then(done);  // ABC

使ってみる

環境

Promiseが動く環境は
ブラウザの対応状況はCan I useで確認でき
Node.jsはv0.11.13から使えます。

new Promise(callback)

Promiseを利用するにはPromiseをnewでインスタンスを作ります。

作ったインスタンスはそのままリターンさせて
実際の処理はCallbackを渡して書いていきます。

そして成功すればresolveを失敗すればrejectを呼び出します。

試しにNode.jsのfs.readFileをラップしたreadFileAsyncを作ってみます。


fs = require('fs');

function readFileAsync(file) {
  return new Promise(function(resolve, reject){
    fs.readFile(file, function(err, data){
      if (err) {
        reject(err); // errがあればrejectを呼び出す
        return;
      }

      resolve(data); // errがなければ成功とみなしresolveを呼び出す
    });
  });
}

Promise.prototype.then(onFulfilled, onRejected)

実装した関数を使う場合は呼び出した関数の戻り値であるPromiseのメソッドthenを呼び出すことで値を受け取れます。

第一引数に成功時呼びたい関数を第二引数に失敗時に呼び出したい関数を入れれます。


// 成功時呼ばれる関数
function onFulfilled(data) {
  console.log(data);
}

function onRejected(err) {
  console.log(err);
}

readFileAsync(module.filename)
  .then(onFulfilled, onRejected);

Promise.resolve(val)

もしthenをさらに繋げたい場合(成功時に複数の処理をさせたい時など)は成功時の関数の戻り値に何かしらの値を返してあげることで繋ぐことができます。


function onFulfilled(data) {
  console.log(data);
  return Promise.resolve(data);
}

readFileAsync(module.filename)
  .then(onFulfilled, onRejected)
  .then(function(data){ console.log(data.toString('utf8')); });

Promise.resolveはnew Promiseを簡略化するためのもので

以下のコードと同等です。

new Promise(function(resolve){
    resolve(val);
});

同じようにPromise.rejectもありこちらはテストコードを書く時に一貫性を保つために使われたりします。(来年はテストコードもバリバリ書きたい)

ちなみにさっきのthenを繋ぐところの何かしらの値というのはPromiseインスタンスじゃなくともPromiseにキャストされるので以下のコードでも同じことになります。

function onFulfilled(data) {
  console.log(data);
  return data;
}

Promise.prototype.catch(onRejected)

Promiseの処理がrejectされた場合thenの第二引数かcatchで登録した関数が呼び出されます。

thenの第二引数に登録する時と後ろにcatchを繋ぐ場合の違いとしては

thenが実行中に例外が発生した場合でもcatchでエラーを取得することができます。

というのも先ほど説明したthen内で返された値やthrowした値がpromiseにキャストしてくれるおかげです。

readFileAsync(module.filename)
  .then(onFulfilled, onRejected); // onFulfilledでthrowされてもonRejectedが実行されない

readFileAsync(module.filename)
  .then(onFulfilled)
  .catch(onRejected); //実行される

readFileAsync(module.filename)
  .then(onFulfilled) // ここでエラー
  .then(undefined, onRejected); // ここは実行される


Promise.all([p1, p2, ...])

Promise.allは配列で渡されたPromiseが全て終わった後に実行したい場合に使います。

渡す配列の中にPromiseじゃないのが混ざっててもPromise.resolveでラップされるので問題なく取得できます。


var files = ['./a.txt', './b.txt', './c.txt'];

Promise.all(files.map(function(file){
    return readFileAsync(file);
  }))
  .then(function(results){ // 結果は配列にまとまって帰ってくる ['a', 'b', 'c']
    return results.map(function(result){
       console.log(result);
       return result;
    });
  })
  .then(...)
  .catch(onRejected); // どれか一つでも失敗すれば呼ばれる

Promise.race([p1, p2, ...])

Promise.raceは渡したPromiseで最初に解決されたものが呼ばれます。

Endpointが複数あった場合リクエストを同時に投げて、先に帰ってきたやつを使うとかそういう使い道ができるのではないかと思います。

Promise.race(files.map(function(file){
    return readFileAsync(file);
  }))
  .then(function(results){ 
    console.log(results); // 'a'
    return results;
  });

IEとかでPromiseを使う

悲しいことにIEでは全くネイティブでPromiseがサポートされていません
なのでes6-promiseを利用します。

es6-promiseはPromiseのPolyfillでPromiseがネイティブで利用できない環境で使えるようにしてくれます。

インストール方法はnpmかbowerのどちらでもいけるようです。

ただIE8以下の環境では.catchって書くとエラーになるので['catch']という風に囲まないといけないようなので注意が必要です。

promise['catch'](function(err){...});

PromiseとCallbackの両方対応させてみる

Callback関数をつけるかつけないかで切り替えてみます。
需要はわからないけど...


function readFileAsync(file, callback) {
  var promise = new Promise(function(resolve, reject){
    fs.readFile(file, function(err, data){
      if (err) {
        reject(err);
        return;
      }

      resolve(data);
    });
  });

  if(!(typeof callback === 'function')){ return promise; }

  promise
  .then(function(data){callback(null, data);})
  .catch(callback);

}

まとめ

Promiseを使うことでCallback地獄を回避しメンテナンス性をあげつつ効率的に処理を進めることができるのでドンドン活用していきたいですね!

あと、本当はPolymerとかMaterialDesignとか書く予定だったんですが

良いサンプルが作れなかったのと

もう少し時間を掛けて作りたいものができたので

その辺を合わせて書く予定です。(できれば今年中に)

参考

This post is the No.5 article of JavaScript Advent Calendar 2014