JavaScriptはプログラマーの人気1位の言語と言われています。ブラウザで唯一動作が保証されているプログラミング言語ですし、サーバーサイドではNode.jsとして動作します。さまざまな分野で使われているプログラミング言語、それがJavaScriptです。
1つのプログラミングを習得すれば、他のプログラミング言語の学習も容易になります。まずJavaScriptをしっかりと学んでみましょう。
この記事ではそんなJavaScriptを学ぶ上で良くある疑問を、なるべく分かりやすく解説します。今回は「async/awaitの使い方」について解説します。
- 非同期処理
- Promiseとは
- async/awaitで表現
- async/awaitの基本
- Promiseの問題
- async/awaitの登場
- 自作関数でasync/awaitを使う
- async/awaitの実例
- async/awaitの向かない処理
- まとめ
非同期処理
基本的にブラウザで実行されるJavaScriptはメインスレッド1つ(処理する経路)となっています。ネットワーク処理やファイル操作を行う際、レスポンスを待つことになります。この待っている間、JavaScriptが処理中のままだと画面が固まってしまいます。
それを防ぐために、非同期処理時にスレッドを停止させないようになっています。それがPromiseです。
Promiseとは
Promiseとは日本語で言えば約束です。たとえば飲食店での食事を思い浮かべてください。多くの場合、以下のような流れではないでしょうか。
- お客が料理を注文
- 調理開始
- 調理終了
- 料理をお客に出す
- 食事終了
- お客は料金を支払う
- すべて完了
たとえば注文と同時に料理は出てきませんし、お金を払うわけではありません。前払いの飲食店もありますが、逆にお金を払った瞬間に料理が出てくる訳でもありません。一連の流れには時間差がありますが、相互に信頼しあうことで成り立っています。
async/awaitで表現
上記の処理をお客視点で async/await
を使って書くと、次のようになるでしょう。
const 料理 = await 料理を注文(); await 食事(料理); const 完了 = await 料金支払い();
このように、非同期処理の完了を待つのが await
の役目になります。
async/awaitの基本
async/await
は以下のようなルールがあります。
- awaitはasyncで定義された関数内でしか使えない
- awaitのレスポンスは処理成功時の内容
つまり、基本形は以下のようになります。
async function Hello() { await Byebye(); }
Promiseの問題
async/awaitは、それまでよく使われていたPromiseを置き換えたものです。つまり、従来は非同期処理を以下のように記述していました。
fetch('/get') .then(function(res) { return res.json(); }) .then(function(data) { console.log(data); }) .catch(function(err) { console.log(err); });
この場合の問題点は、 res
と data
が変数のスコープが区切られてしまって、アクセスできないことです。もしどうしても使いたい場合には let を使うことでしょう。
let res; // 変数の定義 fetch('/get') .then(function(r) { res = r; // 代入 return r.json(); }) .then(function(data) { console.log(res); // <- アクセスできる console.log(data); }) .catch(function(err) { console.log(err); });
もう一つの問題は catch
が1つ目のthenと2つ目のthen、どちらで発生したか分からないことです。もし区別しようと思ったら、ネストをさらに深くする必要があります。これではどんどん分かりづらいコードになってしまいます。
fetch('/get') .then(function(res) { res.json() .then(function(data) { console.log(data); }) // res.json()のエラー .catch(function(err) { console.log(err); }); }) // fetchのエラー .catch(function(err) { console.log(err); });
async/awaitの登場
こうしたネストが深くなる問題を解決したのが async/await
になります。上記のコードを書き直すと、以下のようになります。
try { const res = await fetch('/get'); const data = await res.json(); } catch (err) { console.log(err); }
async/await
では try〜catch
で処理を囲みます(なお、この書き方ではエラーを区別するのには対応していません)。
自作関数でasync/awaitを使う
ネットワーク処理に限らず、非同期処理を使いたくなる場面があります。一番多いのは、処理を数秒待つ(スリープ)処理です。これもasync/awaitを使うと簡単に書けます。
function sleep(second) { return new Promise(function(res) { setTimeout(res, second * 1000); }); } await sleep(5); // 5秒処理停止
このように Promise オブジェクトを使うことで、async/awaitに対応した関数を作成できます。書き方は次の通りです。
function a() { return new Promise(function(res, rej) { // 何かの処理 if (something()) { res(); // 処理成功した場合 } else { rej(); // 処理失敗した場合 } }); } await a();
async/awaitの実例
筆者がよく使うのは、ファイルダイアログで指定された画像を読み込んで、そのサムネイル画像を表示するといった処理です。これは以下のように書けます。
<input type="file" id="file" /> <img src="" id="thumbnail" />
ファイルダイアログでファイルを指定すると、 onchange
イベントが呼ばれます。
document.querySelector('#file').onchange = function(e) { // ファイルオブジェクト const file = e.target.files[0]; // 1つ目のファイル };
この file
オブジェクトのファイル内容を読み取る際にはFileReaderオブジェクトを使います。基本的な使い方は以下のようになります。 onload
が呼ばれる仕組みで、この部分が分かりづらさにつながっています。
const fileReader = new FileReader(); fileReader.onload = function(data) { // ファイルを読み込んだ後の処理を書く } fileReader.readAsDataURL(file);
そこで、このFileReaderを利用する部分を async/await
を使った関数に書き換えます。具体的には以下の通りです。
function readFile(file) { return new Promise(function(res, rej) { const fileReader = new FileReader(); fileReader.onload = res; // 読み込んだらPromiseのresを呼び出す fileReader.readAsDataURL(file); }); }
こうすれば、ファイルを読み込む処理が以下のように簡単に書けます。
document.querySelector('#file').onchange = async function(e) { // 関数はasyncにする // ファイルオブジェクト const file = e.target.files[0]; // 1つ目のファイル const data = await readFile(file); // ファイルの内容を読み込む document.querySelector('#photo').src = data; // 画像の内容を適用する };
async/awaitの向かない処理
async/awaitはすべてに有効とは言えません。処理が終わるまでJavaScriptのコードは止まってしまうので、forなどを使った繰り返し処理の中でawaitを使うのはお勧めできません。
// URLを配列を順番にfetchする for (const url of urls) { await fetch(url); }
非同期処理を繰り返す場合、 await Promise.all
を使うのがお勧めです。
// 一旦処理を実行 const promises = urls.map(function(url) { return fetch(url); }); // Promise.allですべての処理完了を待つ await Promise.all(promises);
まとめ
Promiseからasync/awaitに書き直すと、ネスト(インデント)が減って、全体の処理が見通し良くなります。ぜひ積極的に使っていきましょう。
ブラウザで使うJavaScriptでは、ネットワーク処理が多用されます。async/awaitを使えば、すっきりしたコードになるはずです。