GoogleのPWAのチュートリアルをやってみたらPromise
が利用されていました。
これは私が「そろそろPromise
を理解しないとこれから先ついていけない」と思い、触って確かめてみた備忘録となります。
非同期処理
一般的にプログラムは実行するとコードを上から順番に実行します。
そのため、XHRのような処理時間の長いタスクを実行すると、それが完了するまで次のタスクに進めません。
そこで、非同期処理を用います。
非同期処理は、あるタスクが実行している間に、他のタスクが別の処理を行える方式です。
これにより、例えばネットワークの通信をしている間に、内部の処理を進めるなど、効率的に処理を行うことができます。
例として以下のようなコードを実行します。
const sleep = (callback) => {
setTimeout(() => {
callback(new Date());
}, 1000);
};
console.log('Begin Asynchronous Code');
sleep((date) => {
console.log(`[1] : ${date}`);
sleep((date) => {
console.log(`[2] : ${date}`);
sleep((date) => {
console.log(`[3] : ${date}`);
});
});
});
console.log('End Asynchronous Code');
出力結果は以下のようになります。
"Begin Asynchronous Code"
"End Asynchronous Code"
"[1] : Fri Mar 16 2018 21:02:48 GMT+0900 (JST)"
"[2] : Fri Mar 16 2018 21:02:49 GMT+0900 (JST)"
"[3] : Fri Mar 16 2018 21:02:50 GMT+0900 (JST)"
setTimeout
で待っている間にも処理を行えていることがわかると思います。
しかし、このように単純なコールバック関数で非同期処理を実現すると、処理が増えるに連れてネストも深くなり、コードの見通しが悪くなってしまいます。
このことをCallback Hellと言うとかなんとか…
Promise
Promise
オブジェクトは非同期処理の最終的な結果を表現します。
Promiseオブジェクトの作成例を以下に示します。
const promise = new Promise((resolve, reject) => {
//時間のかかる処理
//
// resolve(/*処理結果など*/);
//もしくは
// reject(/*エラーメッセージなど*/);
});
Promise
コンストラクタはexecutor
という関数を引数とします。
executorは2つの引数resolve
とreject
を取る関数です。
executor関数は非同期処理が完了した際にresolve関数かreject関数のいずれかを呼び出します(そのように設計します)。
resolve関数はPromiseに対してresolve(解決)を、reject関数はPromiseに対してreject(拒否)を行います。
executor関数でエラーが投げられた場合、Promiseはrejectされ、executorの返り値は無視されます。
基本的な使用例
例として、さきほどのコールバック関数のみで記述した非同期処理をPromiseを用いて書き直します。
const sleep = () => {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(new Date());
}, 1000);
});
};
console.log('Begin Asynchronous Code');
sleep()
.then((date) => {
console.log(`[1] : ${date}`);
return sleep();
})
.then((date) => {
console.log(`[2] : ${date}`);
return sleep();
})
.then((date) => {
console.log(`[3] : ${date}`);
});
console.log('End Asynchronous Code');
出力結果はこちら。
"Begin Asynchronous Code"
"End Asynchronous Code"
"[1] : Fri Mar 16 2018 23:21:53 GMT+0900 (JST)"
"[2] : Fri Mar 16 2018 23:21:54 GMT+0900 (JST)"
"[3] : Fri Mar 16 2018 23:21:55 GMT+0900 (JST)"
前述のコードと条件を合わせるために、sleepは呼び出されてから1秒後に時刻を返す関数としています。
このようにPromiseを使えばthen
でPromise
を繋ぐことにより非同期処理を実現できます。
Promiseを用いた記法であれば、ネストを深くすることなく読みやすいコードを書けます。
ハンドラコールバック
Promiseの返り値はPromise.prototype.then
メソッドまたはPromise.prototype.catch
メソッドにより受け取れます。
また、これらのメソッドはPromiseの成功・失敗に応じたハンドラを付加することができます。
then
は引数として成功ハンドラと失敗ハンドラを、catch
は失敗ハンドラのみを取ることができます。
then
先にthenの例を示します。
const promise1 = new Promise((resolve, reject) => {
resolve('Resolve!');
});
const promise2 = new Promise((resolve, reject) => {
reject('Reject!')
});
promise1.then((value) => {
console.log(`[onFulfilled] ${value}`);
}, (reason) => {
console.log(`[onRejected] ${reason}`);
});
promise2.then((value) => {
console.log(`[onFulfilled] ${value}`);
}, (reason) => {
console.log(`[onRejected] ${reason}`);
});
実行結果。
"[onFulfilled] Resolve!"
"[onRejected] Reject!"
このコードではpromise1
はresolve
をpromise2
はreject
をそれぞれ呼び出します。
また、then
は第一引数に成功ハンドラを、第二引数に失敗ハンドラを取ります。
そのためpromise1
は成功ハンドラを呼び出し、promise2
は失敗ハンドラを呼び出しています。
また、thenは引数となるハンドラが足りていない場合でもエラーを生成しません。
次の例を見てください。
const promise = new Promise((resolve, reject) => {
resolve('Resolve!');
});
promise.then((value) => {
console.log(`Only onFulfilled ${value}`);
});
出力。
"Only onFulfilled Resolve!"
エラーハンドリングが必要ない場合、このように失敗ハンドラを省略することでコードを簡素に保つことができます。
また、ほとんど使う機会はないと思いますが、thenの第一引数にundefined
を指定することで明示的に失敗ハンドラのみを付加できます。
catch
catchは失敗ハンドラを付加するメソッドです。
例を見てみましょう。
const promise1 = new Promise((resolve, reject) => {
resolve('Resolve!');
});
const promise2 = new Promise((resolve, reject) => {
reject('Reject!');
});
promise1.catch((reason) => {
console.log(`[promise1] ${reason}`);
});
promise2.catch((reason) => {
console.log(`[promise2] ${reason}`);
});
出力結果です。
"[promise2] Reject!"
promise1
とpromise2
の違いに注目してください。
promise1
はresolve
を返し、promise2
はreject
を返しています。
両方ともにcatch
メソッドを呼び出していますが、ハンドリングされた関数が呼び出されているのはreject
を返すpromise2
のみです。
また、Promiseはエラーを投げた場合もreject
同様に失敗ハンドラを呼び出します。
const promise = new Promise((resolve, reject) => {
throw new Error('!?Error!?');
});
promise.catch((reason) => {
console.log(`[catch] ${reason}`);
});
予期せぬエラーもcatchで拾うことができます。
"[catch] Error: !?Error!?"
複雑なハンドリング
thenとcatchを併用することで複雑なハンドリングを記述できます。
例として類似した2つのコードを作成しました。
const promise = new Promise((resolve, reject) => {
resolve('Resolve!');
});
promise.then((value) => {
console.log(`then1: ${value}`);
return value;
}).then((value) => {
console.log(`then2: ${value}`);
throw new Error('Error!');
}).catch((reason) => {
console.log(`catch1: ${reason}`);
return reason;
}).then((value) => {
console.log(`then3: ${value}`);
});
promiseがresolveを返す結果。
"then1: Resolve!"
"then2: Resolve!"
"catch1: Error: Error!"
"then3: Error: Error!"
const promise = new Promise((resolve, reject) => {
reject('Reject!');
});
promise.then((value) => {
console.log(`then1: ${value}`);
return value;
}).then((value) => {
console.log(`then2: ${value}`);
throw new Error('Error!');
}).catch((reason) => {
console.log(`catch1: ${reason}`);
return reason;
}).then((value) => {
console.log(`then3: ${value}`);
});
promiseがrejectを返す結果。
"catch1: Reject!"
"then3: Reject!"
1つ目のコードではresolveを返すため、上2つのthenを通りcatchが呼び出されています。
2つ目のコードではrejectを返すため、すぐさまcatchが呼び出されてます。
thenやcatchに渡された関数が値を返した場合は、Promiseでラップされます。
そのため、thenやcatchの返り値をthenで取得し、複数の処理を繋ぐことができます。
静的なPromise
Promise.resolve
メソッド及びPromise.reject
メソッドを使用して、それぞれ成功となるPromiseオブジェクトと失敗となるPromiseオブジェクトを返すことができます。
Promise.resolve('Resolve!').then((value) => {
console.log(`then: ${value}`);
});
Promise.reject('Reject!').catch((reason) => {
console.log(`catch: ${reason}`);
});
出力結果。
"then: Resolve!"
"catch: Reject!"
Promise.all
Promise.all
メソッドはiterableな引数をとり、引数内のすべてのPromiseが成功したときに成功となるPromiseを返します。
Promiseが成功した場合、引数に含まれるすべてのPromiseが返した値を配列として返します。
また、引数内のいずれかのPromiseが失敗したとき、すぐさま失敗となるPromiseを返します。
Promiseが失敗した場合、引数に含まれるPromiseのうち、最初に失敗したPromiseの理由を値として返します。
すべてのPromiseが成功した例
const promise1 = Promise.resolve('promise1 resolved');
const promise2 = 'promise2 resolved'
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('promise3 resolved');
}, 1000);
});
Promise.all([promise1, promise2, promise3]).then((value) => {
console.log(value);
});
出力。
["promise1 resolved", "promise2 resolved", "promise3 resolved"]
いずれかのPromiseが失敗した例
const promise1 = Promise.resolve('promise1 resolved');
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('foo');
reject('promise2 rejected');
}, 500);
});
const promise3 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('bar');
reject('promise3 rejected');
}, 1000);
});
Promise.all([promise1, promise2, promise3]).catch((reason) => {
console.log(reason);
});
実行結果。
"foo"
"promise2 rejected"
"bar"
このようにPromise.allメソッドは複数のPromiseの結果を集約するのに役立ちます。
注意点として、いずれかのPromiseが失敗した場合、即座に失敗となるPromiseを返しますが、他の引数内のPromiseは終了されることなくそのまま実行を続けます(実行結果のbar
)。
Promise.race
Promise.race
メソッドはiterableな引数をとり、引数内のPromiseのうち、一つが成功もしくは失敗したときに、すぐさま値や理由とともにPromiseを返します。
返されるPromiseは一番最初に解決もしくは拒否された値のみです。
const promise1 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('foo');
resolve('promise1 resolved');
}, 1000);
});
const promise2 = new Promise((resolve, reject) => {
setTimeout(() => {
console.log('bar');
reject('promise2 rejected');
}, 2000);
});
Promise.race([promise1, promise2]).then((value) => {
console.log(value);
}).catch((reason) => {
console.log(reason);
});
出力結果。
"foo"
"promise1 resolved"
"bar"
上記の例ではpromise1
が先に成功するため、成功時の値のみが返されています。
Promise.race
は一つのPromiseが完了したときすぐに値を返しますが、iterableな引数に含まれる他のPromiseは終了されず、そのまま実行を続けるため注意が必要です。
まとめ
所見では複雑そうに見えたPromiseも、実際に書いてみるとそんなに難しいものではないように思えました。
Promiseを効果的に利用して動作効率・メンテナンス性の高いコードを作っていきたいですね。
今後はPromiseオブジェクトの詳細な動作と、async・awaitなどについても勉強していきたいです。