Help us understand the problem. What is going on with this article?

async/await 入門(JavaScript)

はじめに

今更ですが、JavaScriptのasync/awaitに関する備忘録になります。

  • 「今まで$.Deferred()Promiseなどで非同期処理は書いたことがあるが、async/awaitはわからない」
  • $.Deferred()Promiseなどの非同期処理の書き方より、もっと簡潔に書ける書き方があれば知りたい」
  • 「今までの非同期処理の書き方と比べて何が良いのかわからない」

といった人達向けの記事です。

$.Deferred()Promiseなどで非同期処理を書いたことがある前提のため、非同期処理自体に関する説明は記載しておりません。
記載している利用例のコードはChrome(最新)のコンソール上で動きますので、コンソール上で実行して動作を確認してみると理解が深まりやすいと思います。

本記事で用いている用語

Promiseを返す

Promiseオブジェクトを返すこと。

// Promiseを返す
return new Promise((resolve, reject) => {

});

Promiseの結果を返す

Promiseresolveもしくはrejectを実行すること。

return new Promise((resolve, reject) => {
    // Promiseの結果を返す
    resolve('resolve!!');
});

resolveする

Promiseresolveを実行すること。

return new Promise((resolve, reject) => {
    // succes!!をresolveする
    resolve('succes!!');
});

rejectする

Promiserejectを実行すること。

return new Promise((resolve, reject) => {
    // err!!をrejectする
    reject('err!!');
});

async/awaitとは

asyncawaitを利用した、非同期処理の構文のこと。

何故async/awaitを利用するのか

Promiseを利用した構文よりも、簡潔に非同期処理が書けるから。

async/awaitの対応状況

以下は各ブラウザのasync/awaitの対応状況。

ECMAScript 2016+ compatibility table
async-await.jpg

全てのブラウザで対応しているわけではないため、利用するならBabel等でトランスパイルする必要がある。

asyncとは

非同期関数を定義する関数宣言のこと。
以下のように関数の前にasyncを宣言することにより、非同期関数(async function)を定義できる。

async function sample() {}

async functionasyncで宣言した関数)は何をするのか

  • async functionは呼び出されるとPromiseを返す。
  • async functionが値をreturnした場合、Promiseは戻り値をresolveする。
  • async functionが例外や何らかの値をthrowした場合はその値をrejectする。

言葉だけだとわかりづらいため、利用例を見てみる。

async functionの利用例

以下はasync functionPromiseを返し、値を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 functionPromiseを返し、値をresolve、もしくはrejectしていることがわかった。
上記はasync function単体の利用例だが、awaitと併用して利用することが多く、「asyncを利用するならawaitも必ず利用すべき」と書かれている記事もあった。

awaitとは

async function内でPromiseの結果(resolvereject)が返されるまで待機する(処理を一時停止する)演算子のこと。
以下のように、関数の前にawaitを指定すると、その関数のPromiseの結果が返されるまで待機する。

async function sample() {
    const result = await sampleResolve();

    // sampleResolve()のPromiseの結果が返ってくるまで以下は実行されない
    console.log(result);
}

awaitは何をするのか

  • awaitを指定した関数のPromiseの結果が返されるまで、async function内の処理を一時停止する。
  • 結果が返されたらasync function内の処理を再開する。

awaitasync 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/awaitPromiseを利用しているため、Promise自体の理解は必須です。
理解を深めるためにも以下の一読をオススメします。

また、連続した非同期処理(for文などの中でawait)を利用するうえでの注意点に関しては、以下を一読すればさらに理解が深まるかと思います。

  • async/await地獄(単純に「使いどころを気をつけよう」という記事です。)

お知らせ

KDP(Kindle ダイレクト・パブリッシング)を利用して、「webpack 実践入門」という書籍を出版しました。

https://www.amazon.co.jp/dp/B07X9H8JZZ/

500円(Kindle Unlimited だったら無料)

書籍の詳細は商品ページこちらからご覧いただけます。

興味を持ってくださった方はご購入いただけると大変嬉しいです。よろしくお願いいたします。

Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした