ニフクラ mobile backend(mBaaS)お役立ちブログ

スマホアプリ開発にニフクラ mobile backend(mBaaS)。アプリ開発に役立つ情報をおとどけ!

JavaScriptの基本(その2)「async/awaitの使い方」

JavaScriptはプログラマーの人気1位の言語と言われています。ブラウザで唯一動作が保証されているプログラミング言語ですし、サーバーサイドではNode.jsとして動作します。さまざまな分野で使われているプログラミング言語、それがJavaScriptです。

1つのプログラミングを習得すれば、他のプログラミング言語の学習も容易になります。まずJavaScriptをしっかりと学んでみましょう。

この記事ではそんなJavaScriptを学ぶ上で良くある疑問を、なるべく分かりやすく解説します。今回は「async/awaitの使い方」について解説します。

非同期処理

基本的にブラウザで実行されるJavaScriptはメインスレッド1つ(処理する経路)となっています。ネットワーク処理やファイル操作を行う際、レスポンスを待つことになります。この待っている間、JavaScriptが処理中のままだと画面が固まってしまいます。

それを防ぐために、非同期処理時にスレッドを停止させないようになっています。それがPromiseです。

Promiseとは

Promiseとは日本語で言えば約束です。たとえば飲食店での食事を思い浮かべてください。多くの場合、以下のような流れではないでしょうか。

  1. お客が料理を注文
  2. 調理開始
  3. 調理終了
  4. 料理をお客に出す
  5. 食事終了
  6. お客は料金を支払う
  7. すべて完了

たとえば注文と同時に料理は出てきませんし、お金を払うわけではありません。前払いの飲食店もありますが、逆にお金を払った瞬間に料理が出てくる訳でもありません。一連の流れには時間差がありますが、相互に信頼しあうことで成り立っています。

async/awaitで表現

上記の処理をお客視点で async/await を使って書くと、次のようになるでしょう。

const 料理 = await 料理を注文();
await 食事(料理);
const 完了 = await 料金支払い();

このように、非同期処理の完了を待つのが await の役目になります。

async/awaitの基本

async/await は以下のようなルールがあります。

  1. awaitはasyncで定義された関数内でしか使えない
  2. 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);
    });

この場合の問題点は、 resdata が変数のスコープが区切られてしまって、アクセスできないことです。もしどうしても使いたい場合には 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を使えば、すっきりしたコードになるはずです。

中津川 篤司

中津川 篤司

NCMBエヴァンジェリスト。プログラマ、エンジニアとしていくつかの企業で働き、28歳のときに独立。 2004年、まだ情報が少なかったオープンソースソフトの技術ブログ「MOONGIFT」を開設し、毎日情報を発信している。2013年に法人化、ビジネスとエンジニアを結ぶDXエージェンシー「DevRel」活動をスタート。