Promiseとか非同期処理が意味不明
普段プログラミングで書くのは同期処理です。
コードを順番に上から下に実行していって、途中時間のかかる処理の間は動作・画面が途中で止まることになります。
例えば時間かかる処理としてはサーバーと通信して画像とか動画とかのサイズ大きいデータを取得する処理とかですね。
画像を表示しようとするたびに画面が固まって操作を受け付けないページとかあったら、イライラしてさっさと戻るボタン押しますよね。
このような場合JavaScriptで使うのは非同期処理です!
非同期処理はコードを順番に上から下に実行していくけど、時間がかかる処理は「あとで処理するよ〜」って感じで予約だけして次の処理に移らせることができます。
このようにすればページ全体を表示するような処理はささっと終了させて、あとから画像が読み込まれて表示されるなんてことができますね。
処理の予約ってどうやるのか?
非同期処理で処理の予約だけして実際には処理はせずに、次に進むと書きました。
この処理の予約にはJavaScriptは関数を登録
して実現します。
関数を登録(セット)して後で実行するのをコールバックと言います。
JavaScriptはこのコールバックを多用します。
- ボタンをクリックしたときに行う処理(コールバック)をあらかじめセット。実際に押されたら処理(コールバック)が実行される
- ファイルを選択フォームで読み込み完了した時に行う処理(コールバック)をあらかじめセット。ファイルが読み込めたら行う処理を実行
などなどイベントリスナーとして色々あります。
1. コールバックの罠、コールバック地獄
前置き長かったですのでそろそろコード書きます!
こんなコールバック普通はないと思いますけど数秒ごとに文字が出力されるコードです。
const butotnClick = () => {
setTimeout(function () {
console.log('処理1');
setTimeout(function () {
console.log('処理2');
setTimeout(function () {
console.log('処理3');
setTimeout(function () {
console.log('処理4');
setTimeout(function () {
console.log('処理5');
setTimeout(function () {
console.log('処理6');
}, 2000);
} ,1000);
}, 2000);
} ,1000);
}, 2000);
}, 1000);
}
console.logだけだと使い道などないのでコード少し改造して一定間隔で<li>
要素を追加するコードを書きましたのでよければお試しください。
See the Pen callback hell by kohishibashi (@kohishibashi) on CodePen.
これがコールバック地獄。
コールバックの中に
コールバックがあって、
コールバックがその中にあって、
コードバックがさらにある、、、
という複雑な入れ子状態です。
このサンプルでも最低限の処理しか書いていないのでエラーハンドリングや途中でデータの加工などをすることになり、人間には読めたもんじゃなくなります
ここでようやく登場するのがPromise。こんな読みにくい書き方を改善するために導入されました。
2. Promise使えば入れ子構造→順番に書ける
先ほどのコードバックをもっと読みやすくしたい!ということで使われるのがPromiseです。このPromiseのthen
を使うと入れ子構造を順番に書くことができます。
例えばこんな感じ
先ほどのコンソールに出力するサンプルを省略しないで忠実に書いてみるので少し長くなります💦
new Promise(function(resolve) {
setTimeout(function() {
console.log('処理1');
resolve();
}, 1000)
})
.then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('処理2');
resolve();
}, 2000)
})
})
.then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('処理3');
resolve();
}, 1000)
})
})
.then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('処理4');
resolve();
}, 2000)
})
})
.then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('処理5');
resolve();
}, 1000)
})
})
.then(function() {
return new Promise(function(resolve) {
setTimeout(function() {
console.log('処理6');
resolve();
}, 2000)
})
})
See the Pen [Promise] callback hell by kohishibashi (@kohishibashi) on CodePen.
記述は長くはなりましたが、意味不明な入れ子構造からは卒業して上から順に実行されていくのがわかりやすくなったと思います。
- 最初、Promiseオブジェクトを作る
- Promiseオブジェクトは
.then()
を順番に繋げて書くことができる。 - returnにPromiseオブジェクトを書いているので次も
then()
を使える - 一つ一つ処理の終了を待って処理を進めている
-
resolve()
はその関数の処理の終了させるもの -
resolve()
実行したらthen()
に処理が移る
ついでにthen()の中でPromiseをreturnしない場合待たないですぐ次のthen()
を実行する
2.2 冗長な書き方をリファクタリング
正直さっきのコードでは長すぎてPromise使えねーという感じなので、重複している箇所をまとめてアロー関数も使って書き直してみます。
const addElementWithTimer = (msec, text) => new Promise(
resolve => {
setTimeout(() => {
console.log(text);
resolve();
}, msec)
}
)
addElementWithTimer(1000, '処理1')
.then(() => addElementWithTimer(2000, '処理2'))
.then(() => addElementWithTimer(1000, '処理3'))
.then(() => addElementWithTimer(1000, '処理4'))
.then(() => addElementWithTimer(1000, '処理5'))
.then(() => addElementWithTimer(1000, '処理6'))
See the Pen [Promise2] callback hell by kohishibashi (@kohishibashi) on CodePen.
ここまで書くとコールバック地獄は消えて順番に処理していくのがわかりやすくなると思います。
3. 次の処理に引数を渡す
今回くらいの処理なら普通は使わないですが、then
で順番に処理をしていく時に次のthen
の処理に値を渡したい場合があります。
※ 例えば一つ目でAPIからデータ取得して次のthenの中でデータを使って表示を変えるとか。
これは単純でresolve()
に引数を与えれば次のthenに値を渡すことができます。
const addElementWithTimer = (msec, text, id) => new Promise(
resolve => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
resolve(id);
}, msec)
}
)
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
resolve
の引数にid
を入れることで次のthen
に値を渡しています。
これを繰り返してバケツリレーのように渡すように改造してます。
※ 渡す値が同じだと面白くないのでidに1を足して渡してます。
4. async await
読み方はエイスィンク
アウェイト
です。
最初アスィンク
と読んでいたらエイスィインク
だよ。
って言われて今度はエイウェイト
だと思ってたら、こっちは普通だったというどうでもいい思い出があります
4.1 asyncを関数の前につけるとその関数はawait使える
MDNによると
非同期関数は async キーワードで宣言され、その中で await キーワードを使うことができます。 async および await キーワードを使用することで、プロミスベースの非同期の動作を、プロミスチェーンを明示的に構成する必要なく、よりすっきりとした方法で書くことができます。
うーむわからない。
とりあえず先ほどまで出していた例を書き換えてみる
const addElementWithTimer = (msec, text, id) => new Promise(
resolve => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
resolve(id);
}, msec)
}
)
const butotnClick = async () => {
const res1 = await addElementWithTimer(1000, '処理', 1)
const res2 = await addElementWithTimer(2000, '処理', res1)
const res3 = await addElementWithTimer(1000, '処理', res2)
const res4 = await addElementWithTimer(1000, '処理', res3)
const res5 = await addElementWithTimer(1000, '処理', res4)
const res6 = await addElementWithTimer(1000, '処理', res5)
console.log(`処理終了: ${res6}`)
}
butotnClick()
これを実行すると
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
> "処理終了: 7"
console.logの表示は↓のような埋め込みのcodepenだと出ないので、気になる人はcodepenの本体ページいってみてください。
See the Pen Untitled by kohishibashi (@kohishibashi) on CodePen.
4.2 何が変わったのか?
async
を関数の前におくとその関数でPromiseを返す関数は便利な書き方を使えるようになります。
const sampleFunc = async function() {
// ここからawait使うと便利な書き方になる
// ...
// ...
// この関数内だけ
}
- addElementWithTimerはPromiseを返す関数
- awaitを使うと同期処理のコード書いてるかのように書ける
- await使うとthen使うよりもreturnで戻ってくる値が直感的にすごくわかりやすい
// Before
const butotnClick = () => {
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
}
// After 書いていることは同じ
const butotnClick = async () => {
const res1 = await addElementWithTimer(1000, '処理', 1)
const res2 = await addElementWithTimer(2000, '処理', res1)
const res3 = await addElementWithTimer(1000, '処理', res2)
const res4 = await addElementWithTimer(1000, '処理', res3)
const res5 = await addElementWithTimer(1000, '処理', res4)
const res6 = await addElementWithTimer(1000, '処理', res5)
}
4.2.2 実行順番注意
awaitをつけていない箇所の実行の順番も変わるので注意してください。
const addElementWithTimer = (msec, text, id) => new Promise(
resolve => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
resolve(id);
}, msec)
}
)
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
console.log('終了')
}
const butotnClick2 = async () => {
console.log('開始')
const res1 = await addElementWithTimer(1000, '処理', 1)
const res2 = await addElementWithTimer(2000, '処理', res1)
const res3 = await addElementWithTimer(1000, '処理', res2)
const res4 = await addElementWithTimer(1000, '処理', res3)
const res5 = await addElementWithTimer(1000, '処理', res4)
const res6 = await addElementWithTimer(1000, '処理', res5)
console.log('終了')
}
butotnClick1()
, butotnClick2()
を実行すると
# butotnClick1() then()使ったパターン
> "開始"
> "終了"
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
# butotnClick2() async await使ったパターン
> "開始"
> "処理1"
> "処理2"
> "処理3"
> "処理4"
> "処理5"
> "処理6"
> "終了"
butotnClick1は「終了」がすぐにきています。同期処理のconsole.logは先に処理が終わっていて、非同期処理の箇所が遅れて実行されるからこうなっています。
butotnClick2は同期処理っぽく上から順番に処理してくれていてしっくりきますね。
5. Promiseのエラー処理reject
これまでは処理が成功するパターンだけを見てきました。
処理がうまくいかなかった場合に使うのがreject
です。
- 処理成功の
resolve
を第一引数 - 処理失敗の
reject
は第二引数
にとります。以下は同じコードで書き方が違うだけです。
5.1 二つ目の関数を書いてエラー処理する方法
まずは関数を二つ書く書き方。
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
if (id === 2) reject(id);
resolve(id);
}, msec)
}
)
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(
id => addElementWithTimer(2000, '処理', id), // resolveの処理
id => { console.log(`ID: ${id} 失敗...`) } // こっちがreject
)
}
// 実行
butotnClick1()
id === 2
の時にreject
させます。
これを実行すると
> "開始"
> "処理1"
> "ID: 2 失敗..."
"処理2"
とはならずに2個目の処理が実行されました。
引数のidを渡せるのはresolveと同じ。
5.2 catchを使う方法
どちらかといえばこちらの方が便利にかける。catch
を使えばreject()
されたらcatch
に処理が移ります。
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
if (id === 2) reject(id);
resolve(id);
}, msec)
}
)
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.catch(id => { console.log(`ID: ${id} 失敗...`) })
}
// 実行
butotnClick1()
catch
を追加しています。このように書いた場合2個目のthen()
でrejectが起こるのでcatchに処理が移ります。
これを実行すると
> "開始"
> "処理1"
> "ID: 2 失敗..."
ついでに
一応catchの説明は以上ですが、いろんなサンプル書いてみます。文章読むよりも手を動かした方が理解早いのでこの他にも少し改造して動かしてみてください。
catchの後にthen
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.catch(id => {
setTimeout(() => {
console.log(`ID: ${id} 失敗...`) ;
}, 1000);
})
.then(() => { console.log('これはすぐ実行される') })
}
これを実行するとcatch
で1秒待ってからthen
を実行してくれそうだけどすぐ実行されるから注意
> "開始"
> "処理1"
> "これはすぐ実行される"
> "ID: 2 失敗..."
次のthen
に移るためには、Promiseオブジェクト返す
のと、resolve()
しないといけないのでこのように書く必要がある。
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.catch(id => {
return new Promise((resolve, reject) => {
setTimeout(() => {
console.log(`ID: ${id} 失敗...`) ;
resolve();
}, 1000);
});
})
.then(() => { console.log('Promiseが欲しかった') })
}
return省略できる書き方もあるけどわかりやすさ優先で書きました。
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "Promiseが欲しかった"
関数二つ使ったパターンにcatchも使う
引数二つとcatch
を同時に使うとcatch
は使用されない。
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(
id => addElementWithTimer(2000, '処理', id), // resolveの処理
id => { console.log(`ID: ${id} 失敗...`) } // こっちがreject。これが使われる
)
.catch(id => { console.log(`catch実行されないよ`) })
}
> "開始"
> "処理1"
> "ID: 2 失敗..."
6. Promiseの成功しても失敗しても実行するfinally
これまで処理が成功、失敗する場合のPromiseの書き方を見てきました。
実はもう一つあって、成功しても失敗しても実行するfinally
というものがあります。
finallyを使ってみる
resolve()
されて終了しても、reject()
されて終了しても最後に処理を行ってくれるfinally
の例を見ていきます。
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
if (id === 2) reject(id);
resolve(id);
}, msec)
}
)
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.catch(id => { console.log(`ID: ${id} 失敗...`) })
.finally(()=> { console.log('処理終了!') })
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"
処理が失敗して最後にfinally
の処理が実行されて終了してますね。
次にrejectを消して、catchにならないように実行してみると
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
resolve(id);
}, msec)
}
)
const butotnClick1 = () => {
console.log('開始')
addElementWithTimer(1000, '処理', 1)
.then(id => addElementWithTimer(2000, '処理', id))
.then(id => addElementWithTimer(1000, '処理', id))
.catch(id => { console.log(`ID: ${id} 失敗...`) })
.finally(()=> { console.log('処理終了!') })
}
> "開始"
> "処理1"
> "処理2"
> "処理3"
> "処理終了!"
then
が全部終わって、最後にfinally
の処理が実行されて終了してますね。
用途としては処理の後処理なんかに使えます。例えば読み込み中のグルグルと回る「通信中表示」を消すとか。
7. async awaitのエラー処理rejectはtry catch
thenにcatchがあるならasync awaitを使った書き方にも当然catch
があります。
コードはthen
とcatch
を繋げて書いたときの内容と同じく、idが2になったらreject。引数にidを渡すコード。
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
if (id === 2) reject(id);
resolve(id);
}, msec)
}
)
const butotnClick1 = async () => {
console.log('開始')
try {
res1 = await addElementWithTimer(1000, '処理', 1);
res2 = await addElementWithTimer(1000, '処理', res1);
res3 = await addElementWithTimer(1000, '処理', res2);
} catch(e) {
console.log(`ID: ${e} 失敗...`)
}
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."
同期処理書く感じで書けるので超わかりやすいですね!
8. async awaitでfinally
これは説明するまでもないかもしれませんが一応書きます。
さっきの例と同じく、idが2の時にrejectされるのでcatchに入り、最後にfinally
に入ります。
const addElementWithTimer = (msec, text, id) => new Promise(
(resolve, reject) => {
setTimeout(() => {
console.log(`${text}${id}`);
id++
if (id === 2) reject(id);
resolve(id);
}, msec)
}
)
const butotnClick1 = async () => {
console.log('開始')
try {
res1 = await addElementWithTimer(1000, '処理', 1);
res2 = await addElementWithTimer(1000, '処理', res1);
res3 = await addElementWithTimer(1000, '処理', res2);
} catch(e) {
console.log(`ID: ${e} 失敗...`)
} finally {
console.log('処理終了!');
}
}
// 実行
butotnClick1()
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"
async awaitだと素直に同期処理っぽく書けるのでfinally
書く必要あることがあまりないかもしれないですね。わざわざfinally書かなくても下のように書けます。
const butotnClick1 = async () => {
console.log('開始')
try {
res1 = await addElementWithTimer(1000, '処理', 1);
res2 = await addElementWithTimer(1000, '処理', res1);
res3 = await addElementWithTimer(1000, '処理', res2);
} catch(e) {
console.log(`ID: ${e} 失敗...`)
}
console.log('処理終了!');
}
> "開始"
> "処理1"
> "ID: 2 失敗..."
> "処理終了!"
最後に
ここまでやればとりあえずコードを解読することはできるのではないかと思います!
これが理解できたら静的メソッドも勉強してみたらPromiseは大体問題ないかなと思います!