前置き
JavaScript の async / await / Promise / then(もっというと try, catch とかも)あたり、書けないことはないけど、イメージが掴みづらく自分が何書いてるかよくわからなくなるので、感覚的な・どういう気持ちで書いていけばいいのか、というところを自分なりにメモするものです。
よくわかっていないものを現在進行系で紐解いていくメモなので「〜じゃないですか?」「〜は間違いです」みたいな指摘のされ方をされても、「正直分からないです」「そうですか・・・」とならざるを得ないので、こちらのほうが正しいですとか編集リクエストベースとか、そういう感じでご指摘いただけると大変嬉しいです。
何がわからないか
- 非同期な処理がしたいんじゃなくて、仕方なく非同期で返ってくる値をうまくこなしたいだけなんだ
- 別に同期的な処理をしたくて openFileSync を使ってるんじゃなくて、非同期がわからないから openFileSync を使っている、を解決したい
- Promise がわからん
- とりあえず async / await だと楽になるらしいけど、説明見ると結局 Promise が出てきてわからん
とりあえず非同期で返ってくる値をどうにかしたい
とりあえず非同期で返ってくる値を async / await でどうにかしよう
async
関数の宣言(function)の前につけると非同期な関数ですよということになる
async function getImageWidth(path) {
}
こうすれば非同期関数になる。なんか非同期処理をした感が出る。
この後説明する await で非同期な値を楽に受け取れるんだけど、await は原則として async な関数内でしか使えないので、先に async について説明した。(Chrome などはトップレベルな await に対応している)
await
非同期な関数を呼び出すときに使うと、結果が帰ってくるまで次の行を実行せずに待つ
ので、さっきの async 関数だと、 await をこんな感じに・・・
async function getImageWidth(path) {
const image = await loadImage(path);
return image.width; // loadImage の値が返ってくるまで実行されない
}
これは便利!Promise とかコールバックとか考えずに、ひとまず非同期な値を受け取れた!
(この getImageWidth
は、loadImage
が返ってくるまで何も返さないので、後述するPromiseを返していないが、非同期関数で有ることに注意。この return は暗黙的に Promise.resolve でラッピングされる。)
画像が返ってこなかったら…?
素直に、 try / catch で書けば良い。(これが await の利点)
async function getImageWidth(path) {
try {
const image = await loadImage(path);
return image.width;
}
catch (err) {
return false;
}
}
Promise が顔を出す
async
は別に非同期な値を扱わなくても非同期関数にできる。
async function sampleAsync() {
return "非同期だよ〜";
}
普通の関数と何が違うのかと言うと・・・
async function sample() {
console.log(sampleAsync()); // => Promise{<pending>}
}
でました Promise。これが分からなくて嫌になっちゃうんだよな。
ひとまず回避するには、async な関数なので、await すれば大丈夫。
async function sample() {
console.log(await sampleAsync()); // => "非同期だよ〜"
}
この辺の Promise の気持ちを整理する。
例えば次のコード、
async function sample() {
const image = await loadImage(path);
console.log(image.width); // => 画像が返った後に実行される
}
これは、await
があるので、console.log
は画像が返るまで実行されない
では、以下のように、うっかり await を消すと、画像が返ってなくても console.log
が実行されてしまう。
const image = loadImage(path);
console.log(image.width); // => 画像が返ってなくても実行される
この時点で、 image
の値は決まっていない。
そういう、非同期処理のときに、(もし実際返ってたとしても、論理的に)まだ返ってるかどうかわからない、という値が Promise
と考えるとうまく整理できそう。
Promise って何?って聞かれたら、ざっくりいうと、不確定な値。
最初に書いた
async function sampleAsync() {
return "非同期だよ〜";
}
でも Promise が返ったが、これは return "非同期だよ〜"
が暗黙的に Promise に変換されている。
Promise の使い方は以下のように、値を resolve({ここ})
に詰め込めば良い。
先程のをちゃんと書くと、以下のようになる。
async function sampleAsync() {
return new Promise((resolve, reject) => {
resolve("非同期だよ〜")
})
)
単に非同期な値が受け取れただけじゃなく、いろいろできるようになったのでまとめ。
(これがいわゆる非同期のテキストによく書いてある最初の部分のかんたんなまとめになる)
非同期かんたんなまとめ
- 非同期の難しいところは、ただ単に遅延して値が帰ってくるだけじゃなく、値が返ってくるとき(resolve)と返ってこない時(reject)があること。
await まとめ
- Promise を返す関数であれば、(async って書いてる関数でなくても)await で呼び出せる。
- 成功時はそのままの値、失敗時はtry-catchのcatchで受け取る
async まとめ
async な関数は2つの形がある
- Promise を返している
- await がある
await は async な関数に対して使うので、結局これも Promise があるということになる。
Promise まとめ
- 非同期な値、のような意味
- 非同期が成功すれば
resolve({ここ})
, 失敗時はreject({ここ})
に必要な情報を入れて返す
async / await が無い頃
async / await が無い頃は、普通の関数で Promise という値を返すことで、非同期関数ということにしていた。
await の代わりに、then
を使っていた
sampleAsync().then((value) => {
console.log(value) // => "非同期だよ〜"
})
try / catch の代わりに、catch
を使っていた
sampleAsync().catch((err) => {
console.log(err)
})
なんとなく Promise
もわかってきた。
Q. ところで、 Promise もなかった頃はどうしてたの・・・?
A. ず〜〜っとコールバックを受け取っていた
setTimeout(() => {
console.log("1")
setTimeout(() => {
console.log("2")
setTimeout(() => {
console.log("3")
}, 1000)
}, 1000)
}, 1000)
これが Promise によって
// setTimeout を Promise 化する wrapper
function sleep (time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
sleep(10, console.log("1"))
.then(() => {
console.log("1")
return sleep(1000)
}).then(() => {
console.log("2")
return sleep(1000)
}).then(() => {
console.log("3")
return sleep(1000)
})
そしてこれが async / await によって
// setTimeout を async 化する wrapper
async function sleep (time) {
return new Promise(resolve => {
setTimeout(resolve, time)
})
}
(async () => {
await sleep(1000)
console.log("1")
await sleep(1000)
console.log("2")
await sleep(1000)
console.log("3")
})
とても見やすくなった!
ここまでくれば、ドキュメントを読んでいろいろ解決できるようになるはず
- async / await
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Statements/async_function
- 今回説明しなかった、async の成功/失敗や、並列読み込みに注意
- Promise
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/all
- https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Promise/race
- とりあえず
await
で受けとけば良いんだけど、Promise
についてもちゃんと知っとくと、もうちょっと柔軟に対応できたりもする。
【注意】async な関数は何でもかんでも await で受ければ良いわけじゃない
await をすると、そこで返ってくるまで処理が止まってしまう!
const image = await loadImage(imagePath)
const sound = await loadSound(soundPath)
とすると、画像を取得している間に音楽を並列で取得することができない!
そういうときの解決策が上記記事や、async / await の MDN にも書いてある。(Promise.all
)