はじめに
今更ですが、JavaScriptのasync
/await
に関する備忘録になります。
- 「今まで
$.Deferred()
やPromise
などで非同期処理は書いたことがあるが、async
/await
はわからない」 - 「
$.Deferred()
やPromise
などの非同期処理の書き方より、もっと簡潔に書ける書き方があれば知りたい」 - 「今までの非同期処理の書き方と比べて何が良いのかわからない」
といった人達向けの記事です。
$.Deferred()
やPromise
などで非同期処理を書いたことがある前提のため、非同期処理自体に関する説明は記載しておりません。
記載している利用例のコードはChrome(最新)のコンソール上で動きますので、コンソール上で実行して動作を確認してみると理解が深まりやすいと思います。
本記事で用いている用語
Promise
を返す
Promise
オブジェクトを返すこと。
// Promiseを返す
return new Promise((resolve, reject) => {
});
Promise
の結果を返す
Promise
のresolve
もしくはreject
を実行すること。
return new Promise((resolve, reject) => {
// Promiseの結果を返す
resolve('resolve!!');
});
resolve
する
Promise
のresolve
を実行すること。
return new Promise((resolve, reject) => {
// succes!!をresolveする
resolve('succes!!');
});
reject
する
Promise
のreject
を実行すること。
return new Promise((resolve, reject) => {
// err!!をrejectする
reject('err!!');
});
async
/await
とは
async
とawait
を利用した、非同期処理の構文のこと。
何故async
/await
を利用するのか
Promise
を利用した構文よりも、簡潔に非同期処理が書けるから。
async
/await
の対応状況
以下は各ブラウザのasync
/await
の対応状況。
ECMAScript 2016+ compatibility table
全てのブラウザで対応しているわけではないため、利用するならBabel等でトランスパイルする必要がある。
async
とは
非同期関数を定義する関数宣言のこと。
以下のように関数の前にasync
を宣言することにより、非同期関数(async function
)を定義できる。
async function sample() {}
async function
(async
で宣言した関数)は何をするのか
-
async function
は呼び出されるとPromise
を返す。 -
async function
が値をreturn
した場合、Promise
は戻り値をresolve
する。 -
async function
が例外や何らかの値をthrow
した場合はその値をreject
する。
言葉だけだとわかりづらいため、利用例を見てみる。
async function
の利用例
以下はasync function
がPromise
を返し、値をresolve
、もしくはreject
しているか確認するための利用例。
※このように利用することはほとんどないと思いますが、async function
がどのような動きをしているのかを確認するために記載しております。
// resolve1!!をreturnしているため、この値がresolveされる
async function resolveSample() {
return 'resolve!!';
}
// resolveSampleがPromiseを返し、resolve!!がresolveされるため
// then()が実行されコンソールにresolve!!が表示される
resolveSample().then(value => {
console.log(value); // => resolve!!
});
// reject!!をthrowしているため、この値がrejectされる
async function rejectSample() {
throw new Error('reject!!');
}
// rejectSampleがPromiseを返し、reject!!がrejectされるため
// catch()が実行されコンソールにreject!!が表示される
rejectSample().catch(err => {
console.log(err); // => reject!!
});
// resolveErrorはasync functionではないため、Promiseを返さない
function resolveError() {
return 'resolveError!!';
}
// resolveErrorはPromiseを返さないため、エラーが発生して動かない
// Uncaught TypeError: resolveError(...).then is not a function
resolveError().then(value => {
console.log(value);
});
上記の通り、async function
がPromise
を返し、値をresolve
、もしくはreject
していることがわかった。
上記はasync function
単体の利用例だが、await
と併用して利用することが多く、「async
を利用するならawait
も必ず利用すべき」と書かれている記事もあった。
await
とは
async function
内でPromise
の結果(resolve
、reject
)が返されるまで待機する(処理を一時停止する)演算子のこと。
以下のように、関数の前にawait
を指定すると、その関数のPromise
の結果が返されるまで待機する。
async function sample() {
const result = await sampleResolve();
// sampleResolve()のPromiseの結果が返ってくるまで以下は実行されない
console.log(result);
}
await
は何をするのか
-
await
を指定した関数のPromise
の結果が返されるまで、async function
内の処理を一時停止する。 - 結果が返されたら
async function
内の処理を再開する。
await
はasync function
内でないと利用できないため、async
/await
の利用例を見ていく。
async
/await
の利用例
以下は単純なasync
/await
の利用例。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value * 2);
}, 2000);
})
}
/**
* sampleResolve()をawaitしているため、Promiseの結果が返されるまで処理が一時停止される
* 今回の場合、2秒後にresolve(10)が返ってきてその後の処理(return result + 5;)が再開される
* resultにはresolveされた10が格納されているため、result + 5 = 15がreturnされる
*/
async function sample() {
const result = await sampleResolve(5);
return result + 5;
}
sample().then(result => {
console.log(result); // => 15
});
上記の処理をPromise
の構文で書くと以下のようになる。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value * 2);
}, 2000);
})
}
function sample() {
return sampleResolve(5).then(result => {
return result + 5;
});
}
sample().then(result => {
console.log(result); // => 15
});
2つを見比べてみると、async
/await
の方が簡潔に書けることがわかる。
より複雑な処理の場合でもasync
/await
を利用した方が簡潔に書けるため、いくつか例を紹介していく。
連続した非同期処理
連続した非同期処理(Promise
構文)
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 1000);
})
}
function sample() {
let result = 0;
return sampleResolve(5)
.then(val => {
result += val;
return sampleResolve(10);
})
.then(val => {
result *= val;
return sampleResolve(20);
})
.then(val => {
result += val;
return result;
});
}
sample().then((v) => {
console.log(v); // => 70
});
連続した非同期処理(async
/await
構文)
await
を利用すれば、then()
で処理を繋げなくても連続した非同期処理が書ける。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 1000);
})
}
async function sample() {
return await sampleResolve(5) * await sampleResolve(10) + await sampleResolve(20);
}
async function sample2() {
const a = await sampleResolve(5);
const b = await sampleResolve(10);
const c = await sampleResolve(20);
return a * b + c;
}
sample().then((v) => {
console.log(v); // => 70
});
sample2().then((v) => {
console.log(v); // => 70
});
for
を利用した繰り返しの非同期処理も書ける。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 1000);
})
}
async function sample() {
for (let i = 0; i < 5; i += 1) {
const result = await sampleResolve(i);
console.log(result);
}
return 'ループ終わった。'
}
sample().then((v) => {
console.log(v); // => 0
// => 1
// => 2
// => 3
// => 4
// => ループ終わった。
});
array.reduce()
も利用できる。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 1000);
})
}
async function sample() {
const array = [5, 10, 20];
const sum = await array.reduce(async (sum, value) => {
return await sum + await sampleResolve(value) * 2;
}, 0);
return sum;
}
sample().then((v) => {
console.log(v); // => 70
});
連続の非同期処理は、処理を順番に行う必要がない限り利用すべきではないため注意。
例えば、画像を非同期読み込みをする場合、上記のような処理だと1つの画像を読み込みが完了するまで、次の画像の読み込みが始まらない。
そのため、全ての画像の読み込みにかなりの時間がかかる。画像は連続して読み込む必要はないため、後述する並列の非同期処理で読み込むべき。
並列の非同期処理
並列の非同期処理(Promise
構文)
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 2000);
})
}
function sampleResolve2(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value * 2);
}, 1000);
})
}
function sample() {
const promiseA = sampleResolve(5);
const promiseB = sampleResolve(10);
const promiseC = promiseB.then(value => {
return sampleResolve2(value);
});
return Promise.all([promiseA, promiseB, promiseC])
.then(([a, b, c]) => {
return [a, b, c];
});
}
sample().then(([a, b, c]) => {
console.log(a, b, c); // => 5 10 20
});
並列の非同期処理(async
/await
構文)
Promise.all
にもawait
を利用できるため、以下のように記述できる。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 2000);
})
}
function sampleResolve2(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value * 2);
}, 1000);
})
}
async function sample() {
const [a, b] = await Promise.all([sampleResolve(5), sampleResolve(10)]);
const c = await sampleResolve2(b);
return [a, b, c];
}
sample().then(([a, b, c]) => {
console.log(a, b, c); // => 5 10 20
});
array.map()
も利用できる。
function sampleResolve(value) {
return new Promise(resolve => {
setTimeout(() => {
resolve(value);
}, 2000);
})
}
async function sample() {
const array =[5, 10, 15];
const promiseAll = await Promise.all(array.map(async (value) => {
return await sampleResolve(value) * 2;
}));
return promiseAll;
}
sample().then(([a, b, c]) => {
console.log(a, b, c); // => 10 20 30
});
例外処理(エラーハンドリング)
例外処理(エラーハンドリング)Promise
構文)
function throwError() {
return new Promise((resolve, reject) => {
setTimeout(function() {
try {
throw new Error('エラーあったよ');
resolve('エラーなかった');
} catch(err) {
reject(err);
}
}, 1000);
});
}
function errorHandling() {
return throwError()
.then((result) => {
return result;
})
.catch((err) => {
throw err;
});
}
errorHandling().catch((err) => {
console.log(err); // => エラーあったよ
});
例外処理(エラーハンドリング)async
/await
構文)
await
を利用すれば、非同期処理のtry catch
を書ける。
function throwError() {
return new Promise((resolve, reject) => {
setTimeout(function() {
try {
throw new Error('エラーあったよ')
resolve('エラーなかった');
} catch(err) {
reject(err);
}
}, 1000);
});
}
async function errorHandling() {
try {
const result = await throwError();
return result;
} catch (err) {
throw err;
}
}
errorHandling().catch((err) => {
console.log(err); // => エラーあったよ
});
実はtry catch
で囲まなくても上記のコードと同様に動く。
function throwError() {
return new Promise((resolve, reject) => {
setTimeout(function() {
try {
throw new Error('エラーあったよ')
resolve('エラーなかった')
} catch(err) {
reject(err);
}
}, 1000);
});
}
async function errorHandling() {
// throwErrorがrejectを返したら処理が中断される
const result = await throwError();
return result;
}
errorHandling().catch((err) => {
console.log(err); // => エラーあったよ
});
終わり
フロントエンド実装で非同期処理は避けられないため、より簡潔に書けるasync
/await
を覚えておいて損はないと思います。
とはいえasync
/await
もPromise
を利用しているため、Promise
自体の理解は必須です。
理解を深めるためにも以下の一読をオススメします。
また、連続した非同期処理(for
文などの中でawait
)を利用するうえでの注意点に関しては、以下を一読すればさらに理解が深まるかと思います。
- async/await地獄(単純に「使いどころを気をつけよう」という記事です。)
お知らせ
Udemy で webpack の講座を公開したり、Kindle で技術書を出版しています。
Udemy:
webpack 最速入門(10,800 円 -> 2,000 円)
Kindle(Kindle Unlimited だったら無料):
React Hooks 入門(500 円)
興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。