はじめに
普段Angularを利用していますが、いまいち非同期処理(Observable等)が理解できなかったので、調べたことをまとめました。
筆者のレベルとしては
- 非同期処理の概念は理解している
- Observableをsubscribeすれば値が取得できることは知っている
- PromiseとObservableの違いがわからない
- RxJS?
- オペレーター(map等)って聞いたことあるけど、どう使えば良いかわからない
くらいのレベルです。
なので、この記事の対象層も「なんか動くけどよくわからない」くらいの人を想定しています。
今回はJavaScriptのPromiseを中心に整理しました。
非同期処理について
「時間がかかる処理を待っている間に別の処理をやったほうがいいよね」という処理です。
以下のような例が挙げられます。
console.log('start');
// 1000ms待つ
setTimeout(() => {
console.log('time out');
}, 1000);
console.log('end');
ログは
start
end
time out
となり、setTimeout
で'time out'というログの出力を1000ms待っている間に、'end'というログが出力されているのがわかります。
setTimeout
によって、非同期処理が終わったタイミング(1000ms経過したタイミング)で、コールバック関数(() => {console.log('time out');}
)が実行されます。
そのコールバック関数が終わった時点でまた別のコールバックを呼ぼうとすると、以下のようになります。
console.log('start');
setTimeout(() => {
console.log('time out 1');
setTimeout(()=>{
console.log('time out 2');
// 次のコールバックへ...
}, 1000);
}, 1000);
console.log('end');
いわゆるコールバック地獄です。ネストが深くなり、エラー処理も煩雑になります。
そのような非同期処理を扱いやすくするためのオブジェクトがPromiseです。
Promise
「非同期処理を管理し、成否によって渡されたコールバック関数を実行するオブジェクト」というようなイメージです。
例としては以下のような使い方になります。
console.log('start');
new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve();
}, 100);
})
.then(()=>{
console.log('time out 1');
})
.then(()=>{
console.log('time out 2');
});
console.log('end');
では、何をやっているのかを見ていきます。
Promiseの作成
まずはPromiseオブジェクトのコンストラクタから見ていきます。
new Promise( /* executor */ function(resolve, reject) { ... } );
引数にはresolveとrejectの二つの関数を引数にとり、非同期処理が成功するとresolve関数を、失敗するとreject関数を実行する関数を渡します。
サンプルでは、setTimeout
で100ms待ってからresolve関数を実行する関数を渡しています。
then()
Promiseオブジェクトを作成後、then()を呼び出すことで非同期処理終了後の処理を渡すことができます。
Promise.then(onFulfilled[, onRejected])
非同期処理が成功するとonFulfilled
関数がresolve関数として、失敗するとonRejected
関数がreject関数として実行されます(onRejected
はオプション)。
サンプルでは、Promiseに渡した関数(executor)が100ms待ったらresolveを呼んでいるので、100ms後にthenメソッドに渡したonFulfilled
関数(console.log('time out 1')};
)が実行されます。
thenは実行されると新たなPromiseオブジェクトを返すので、さらにthen()を繋ぐことが可能になります(Promiseチェーン)。
ネストが深くならず、またcatchやfinallyメソッドを使用すればエラー処理も可能なので、コールバック地獄を避けることができます。
new Promise((resolve, reject)=>{
setTimeout(()=>{
resolve();
}, 100);
})
.then(()=>{
console.log('time out 1');
// 次のthenに値を渡すことができる
return 'next time out';
})
.then((value)=>{
// next time outが渡される
console.log(value);
})
.then(()=>{
// また別の処理へ...
});
async, await
Promiseを簡潔に書く方法として、async/await宣言が存在します。
async
関数宣言時にasyncを加えることで、その関数が返す値はPromiseになります。
つまり、通常の値を返す関数にasync宣言を加えると、非同期関数宣言になります。
// 'async function'を返す非同期処理関数
async function asyncSample(){
return 'async function';
}
// async宣言があるのでPromiseを返す
asyncSample().then((value)=>{
// 'async function'
console.log(value);
})
asyncだけをみると「素直にPromiseを返せばいいじゃん」となりますが、次にみるawaitとともに使用することが前提となっています。
await
awaitはasync宣言された関数中で使用でき、Promiseの終了を待つことができます。
つまり、thenを利用したチェーンを使用しなくても連続した処理を書くことができます。
// setTimeoutでPromiseを返すためのラッパー
// 渡したミリ秒だけ待ち、resolve関数を実行する
function wait(ms){
return new Promise(resolve => {
setTimeout(()=>{
resolve();
}, ms);
})
}
// async宣言をした非同期処理
async function asyncSample(){
// 100ミリ秒待ってtime out 1をログに表示
await wait(100).then(()=>{console.log('time out 1')});
// 1000ミリ秒待ってtime out 2をログに表示
await wait(1000).then(()=>{console.log('time out 2')});
}
console.log('start');
asyncSample();
console.log('end');
ログは
start
end
time out 1
time out 2
となります。もっと複雑な処理だと実感しやすいのですが、簡潔に記述することが可能です。
ちなみにPromiseで書き換えると以下のようになります。
function promiseSample(){
wait(1000)
.then(()=>{console.log('time out 1')
})
.then(()=>{wait(100).then(()=>console.log('time out 2'))});
}
console.log('start');
promiseSample();
console.log('end');
まとめ
- 頑張って書こうとするとコールバック地獄に陥る
- Promiseは非同期処理を見通しよく書くためのオブジェクト
- async, awaitはより簡潔に非同期処理を書くための宣言
間違い・指摘等があればコメントお願いします。
次回はRxJSについて整理していきます。