はじめに
async, awaitについて、初心者(=ちょっと前の自分)の方にも分かりやすくまとめました。
async/awaitはPromiseが分かればすぐ分かるし、Promiseを理解していないとかなり難しい、という印象です。
Promiseについて忘れてしまったな、まだ不十分だなと感じる方は、私の過去の記事僕なりのPromise入門を読んでからこの記事を読むと良いんじゃまいか
async, awaitはPromiseを簡単に扱えるようにしてくれるもの
async, awaitは、Promiseを簡単に扱えるようにしてくれるものです。Promiseだけでも同じことが出来るのである意味使わなくても構わないのですが、使うとよりすっきりとしたコードになります。おそらく簡単に書けることこそがasync, awaitが実装された大義名分です。さっそく順番に見ていきましょう!
asyncとは
async とはfunctionの定義の前につけられる予約語で、次のように使えます。
async function asyncFunc (arg) {
// もちろん引数のargはここで使えます
const message = 'Hello async';
return message;
}
上の関数は、Promiseで書くと次のようになります。
function asyncFunc (arg) {
return new Promise((resolve) => {
// もちろん引数のargはここで使えます
const message = 'Hello async';
resolve(message);
return;
});
}
つまり、
- asyncで定義した関数は、promise(Promiseのインスタンス)を返します。
- asyncで定義した関数の中身は、実は返すpromiseの、コンストラクタの引数の関数の中に書かれていることになります。そして、Promiseのルール通り、その関数は同期的に実行されます。
- asyncで定義した関数の中身の返り値は、実は返すpromiseがresolveする値になります(その値でpromiseがresolveされるということ)。
イメージですが、次のような関係が成り立ちます。
async = 「return new Promise」 と 「resolve」
awaitとは
awaitとは、asyncを用いた関数内のみで使える予約語です。
非同期的処理を同期的処理にように書けるようにしてくれるもの、という風に説明されます。
実際そういう感覚で書いても正しく動くことが多いのですが、結局asyncと同じようにPromiseを簡単に扱えるようにしてくれるものに過ぎず、Promiseが分かっていないと詰まることがあります。また、既出のパーツだけでawaitと同じことができます。
はじめに、awaitの入った関数の例を見ます。
(axios.getは、jQueryの$.getのようなものと思ってください。)
async function asyncFunc () {
const res1 = await axios.get('/some/path1');
const res2 = await axios.get(`/some/${res1.data.nextpath}`);
console.log(res1.data + res2.data); // => "some data"
}
このように、awaitは非同期処理を同期処理にように順番に書くことが出来ます。
なにが起きているのでしょうか。
順番に説明します。
awaitは、typeofのように右側に1変数をとります。
そして右側の式を同期的に実行し、返り値を見ます。
そして、返り値でresolveしたpromiseを生成し、awaitの左側および下のコード全てをthenの引数の関数として実行します。このとき、thenの呼び出し元は生成したpromiseです。
しかし思い出してもらいたいのが、別のpromise(promise1)でresolveする場合、resolve元のpromiseはすぐにfulfilled状態になるのではなく、promise1の状態に固定(lock in)されます。
なので、上の例ではaxios.getはリクエストが終わったらfulfilledになるpromiseを返しているため、リクエストが終わるたびに次に進むということになります。
別の例
async function asyncFunc () {
const res = await axios.get('/some/path');
console.log(res.data); // => "some data"
}
// async関数の返り値はpromiseなので、thenが呼べますね
asyncFunc().then(() => {
console.log(1); // => 1
});
console.log(0); // => 0
実行結果
0
// 少し遅れて
"some data"
1
promiseを返さない場合も、その値でresolveされたpromiseが作られ、awaitの左側および下側がthenの引数関数にくるまれ、それが非同期的に呼ばれます。引数は、resolveされた値がawaitの左側に出てくる形になります。
言葉での説明は分かりにくいので、awaitを含まない形に例を書き換えます。
async function asyncFunc () {
axios.get('/some/path').then((res) => {
console.log(res.data); // => "some data"
});
}
asyncFunc().then(() => {
console.log(1); // => 1
});
console.log(0); // => 0
awaitが入っていると、そのasync関数の同期的な帰り値は必ずpendingなpromiseになります。
awaitが複数あった場合は、その都度thenが足されていく感じです。
async function asyncFunc () {
const res1 = await axios.get('/some/path1');
const res2 = await axios.get('/some/path2');
console.log(res1.data + res2.data); // => "some data"
}
asyncFunc().then(() => {
console.log(1); // => 1
});
console.log(0); // => 0
async function asyncFunc () {
axios.get('/some/path1').then((res) => {
const res1 = res;
axios.get('/some/path1').then((res) => {
const res2 = res;
console.log(res1.data + res2.data);
});
})
}
asyncFunc().then(() => {
console.log(1); // => 1
});
console.log(0); // => 0
※thenの引数の関数がpromiseを返す場合、thenチェーンが先に進むのは返されたpromiseがfulfilledされたタイミングです。
かなり複雑です。ここから、さらにasyncを外すことももちろんできます。
awaitの右側がpromiseを返さない場合、その値をただ左に返します。ですが、やはりawaitの左側は非同期の側(書き換えたらthenの引数関数の中)なので、awaitの左側以降は同期的処理はされません。
async function asyncFunc () {
console.log(0)
const two = await 2;
console.log(two);
}
asyncFunc();
console.log(1);
実行結果
0
1
2
書き換え
async function asyncFunc () {
console.log(0);
Promise.resolve(2).then((two) => {
console.log(two);
});
}
asyncFunc();
console.log(1);
僕のイメージでいうと、
await = その度にその先の部分を全て新しい大きなthenに入れる。
async/awitについて理解すべき点は、基本はこれだけです。
間違いなどありましたら指摘お願いします!