今年のはじめの方から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とか書く予定だったんですが
良いサンプルが作れなかったのと
もう少し時間を掛けて作りたいものができたので
その辺を合わせて書く予定です。(できれば今年中に)