以前も async
, await
, Promise
関係の記事を書いたことがあるのですが、「中で何をやっているかを何となく分かるように」書いた記事だったので、今回は「外から見てどうやって使うかを分かるように」書きたいと思います。
参考「Promise, async, await がやっていること (Promise と async は書き換え可能?) - Qiita」
1. promise は整理券のようなもの
非同期処理は、「処理を開始するが、いつ終わるか分からない」ような処理のことです。
promise は、非同期処理の「整理券」と考えると分かりやすいと思います。
例えば、通信してコンテンツを取得する fetch()
は promise を返しますが、これは「コンテンツを取得する」という注文を受けて通信を開始するものの、いつ終了するか分からないため、promise という整理券を返します。
await promise
をすることで、その整理券の注文の処理が完了するのを待って、コンテンツを受け取ります。
await
を使うということは、await
が記述されているプログラムそのものもいつ終了するか分からないため、promise にする必要があります。
await
が中に記述されている関数 (アロー関数を含む) に async
を付けると、promise を返すようになります。
async
が付けられた関数を async
関数と呼びます。
//
const getJsonSync = url => {
const response = fetchSync(url);
const data = response.jsonSync();
return data;
};
//
const data = getJsonSync('https://example.com/get/something');
console.log(data.something);
//
const getJson = async url => { // getJson() は promise を返す
const response = await fetch(url); // fetch() が返した promise の処理が完了するのを待つ
const data = await response.json(); // json() が返した promise の処理が完了するのを待つ
return data;
};
//
const data = await getJson('https://example.com/get/something'); // getJson() が返した promise の処理が完了するのを待つ
console.log(data.something);
Deno ではコード全体が非同期になるため、上記の書き方がそのまま使用できます。
WEB ブラウザの JavaScript では実行中のスクリプトがいつ終了するのか分からないと困るため、await
を使用しているコード全体を async
関数で囲う必要があります。
(async () => {
//
const getJson = async url => { // getJson() は promise を返す
const response = await fetch(url); // fetch() が返した promise の処理が完了するのを待つ
const data = await response.json(); // json() が返した promise の処理が完了するのを待つ
return data;
};
//
const data = await getJson('https://example.com/get/something'); // getJson() が返した promise の処理が完了するのを待つ
console.log(data.something);
})(); // promise を返しているが await していないため、いつ処理が終了するか分からないまま次に進んでいる
2. promise でないものを promise にしたいときに new Promise()
する
2.1. 基本
比較的新しい JavaScript の機能やライブラリでは初めから promise を返すようになっているため、ほぼ async
, await
だけで書けますが、古い JavaScript の機能や一部のライブラリでは「コールバック関数」を用いて非同期処理をしているため await
できません。
そこで new Promise()
を使用することで、コールバック関数による非同期処理を promise にすることができます。
const promise = new Promise((resolve, reject) => {
// ...
// 処理が終了したら resolve(返り値) を呼ぶ
// エラーが発生したら reject(エラー情報) を呼ぶ
});
2.2. 例: 画像読み込み
以前は以下のようにして画像の読み込みが行われていました。
// 画像読み込みを開始しているが、いつ画像が表示されるか分からない
const image = new Image();
image.onload = () => {
// ... 画像表示処理
};
image.onerror = event => {
// ... 読み込みエラー時の処理
};
image.src = 'image.png';
// ...
// ソースコードのここの時点で、画像が表示されているかどうかは分からない
//
const promise = new Promise((resolve, reject) => {
const image = new Image();
image.onload = () => resolve(image);
image.onerror = event => reject(event);
image.src = 'image.png';
});
//
const image = await promise;
// ... 画像表示処理
// ソースコードのここの時点で、画像は表示されている
現在は new Promise()
を書かずに promise として扱うことが出来ます。
const image = new Image();
image.src = 'image.png';
await image.decode();
// ... 画像表示処理
// ソースコードのここの時点で、画像は表示されている
参考「最新の JavaScript で Image を読み込む方法 - Qiita」
3. new Promise()
は処理したいときにその都度 new
する
既に await
して完了した promise を再度 await
しても、その promise に割り当てられている処理は再実行されないため、promise は使いまわすことができません。
async
関数は呼ぶたびに新たな promise が生成されますが、new Promise()
の場合は毎回 new
してください。
//
const sleepPromise = new Promise(resolve => {
setTimeout(resolve, 1000);
});
//
console.log('a');
await sleepPromise; // 1 秒待機
console.log('b');
await sleepPromise; // 1 秒待機してくれない
console.log('c');
//
const sleep = () => new Promise(resolve => {
setTimeout(resolve, 1000);
});
//
console.log('a');
await sleep(); // 1 秒待機
console.log('b');
await sleep(); // 1 秒待機
console.log('c');
4. promise.then()
は使わない
promise には then()
というメソッドがありますが、これを使いやすくしたものが await
なため、使わないべきです。
「Promise チェーン」は古い書き方です。
JavaScript が今でも prototype
を内部的に使用していてもコードを書くときは class
構文を使用するのと同様です。
5. promise.catch()
は場合によっては使う
then()
と同様に catch()
というメソッドもありますが、こちらはコードのテクニックとして使用した方が便利な場合があります。
非同期処理で発生したエラーをスルーして無視したい場合に使用すると良いです。
//
const getJson = async url => {
const response = await fetch(url);
const data = await response.json();
return data;
};
//
const data = await getJson('https://example.com/get/something').catch(() => ({ something: '(default)' }));
console.log(data.something);
少し応用的な使い方として、Promise.all()
でまとめて promise を並列実行するとき、一部の promise だけエラーを無視することも可能です。
const data = await Promise.all([
getJson('https://example.com/get/required1'),
getJson('https://example.com/get/required2'),
getJson('https://example.com/get/required3'),
getJson('https://example.com/get/optional1').catch(() => null),
getJson('https://example.com/get/optional2').catch(() => null)
]);