JavaScriptの非同期処理を制御する方法


はじめに

今回はJavaScriptの非同期処理について解説していきます。

最初に非同期処理とはという解説をした後に、Promiseを使って制御する方法とasync/awaitを使って制御する方法を解説します。

頑張っていきましょう。

非同期処理とは

そもそも非同期処理とは一体なんでしょうか。

以下のサイトに分かりやすい図があったので拝借しました。

I-26-2. 非同期処理と同期処理の実装パターンと特徴

非同期処理を端的に言うなら、あるタスクが実行しているときに、他のタスクが別の処理を実行できる方式ということができます。

プログラムを例にして、詳しく解説していきます。

普通、プログラムを実行すると、コードを上から順に一行ずつ実行していきます。

これは普段から当たり前のように使っている同期処理と呼ばれるものです。

しかし、サーバーと通信を行った時などは、この同期処理とは違った挙動をします。

具体的には、サーバーと通信を行う(fetchなど)行を実行したあと、その終了を待たずに次の行が実行されることになります。

例えば、fetchを使ってサーバーからデータを取ってきた後に、そのデータを出力するというプログラムを書いたとします。

その時、非同期処理によりfetchが完了する前に次のコードが実行されるため、データが出力できない(undefainedになる)です。

以下のコードで、非同期処理を制御せずにfetchメソッドを用いてgithubからユーザーIDを取得して、出力してみましょう。ちなみに、外部からメソッドをimportするためESModuleであることをpackage.json内に示しておきましょう。

package.json
{"type": "module"}
import fetch from 'node-fetch';

const getGitUsername = () => {
    const url = 'https://api.github.com/users/hangi4343'

    fetch(url).then(res => res.json())
    .then(json => {
        console.log('非同期処理成功')
        return json.login
    }).catch(error => {
        console.error('非同期処理失敗', error)
        return null
    })
}

const message = 'GitのユーザIDは'
const username = getGitUsername()
console.log(message + username)

GitのユーザIDはundefined
非同期処理成功

const username = getGitUsername()の行でgetGitUsername関数の完了を待たずに、その下のconsole.log(message + username)が実行されるためusernameがundefainedになってしまいます。

fetchの使い方についてはこちらの記事を参考にして下さい。

簡単に解説すると、fetchが返すResponseオブジェクトを最初のthenで受け取り、その中でres.json()をreturnしています。その後、それをまたthen受け取り、メッセージを出力した後にjson.loginがreturnされます。また、この処理内でエラーが起きると、そのエラーをcatchで受け取り、その後の処理を実行します。

このコードが上手く実行されるために、const username = getGitUsername()の非同期処理を行う処理を、完了まで待ってから次のコードを実行することが考えられます。

この非同期処理の完了を待つために、Promiseやasync/awaitが用いられます。

Promiseで非同期処理を制御

それでは、非同期処理を制御していきましょう。

以下の記事を参考にしました。

Promiseについて0から勉強してみた

僕なりのPromise入門

Promiseはresolveとrejectの2つの関数を引数に取ります。

  • resolve : 処理が成功したときのメッセージを表示する関数
  • reject : 処理が失敗したときのメッセージを表示する関数
import fetch from 'node-fetch';

const getGitUsername = () => {
    return new Promise((resolve, reject) => {
        const url = 'https://api.github.com/users/hangi4343'

        fetch(url).then(res => res.json())
            .then(json => {
                console.log('非同期処理成功')
                return resolve(json.login)
            }).catch(error => {
                console.error('非同期処理失敗', error)
                return reject("reject")
            })
    })
}

const message = 'GitのユーザIDは'
getGitUsername().then(username => {
    console.log(message + username)
})

非同期処理成功
GitのユーザIDはhangi4343

非同期処理を制御することができました。

コードについて解説します。

Promiseは、resolveとrejectの2つの引数を持ちます。

getGitUsernameを実行すると、Promiseオブジェクトがreturnされます。

このPromiseオブジェクトに対してthenを用いると、そのthenの引数にPromiseオブジェクトのresolveでreturnされた値が代入されます。

もう少し詳しく解説します。Promiseのコンストラクタの中でresolveが呼ばれると、Promiseはresolveの状態になります。thenはPromiseの状態がresolveになったときに呼ばれるので、結果的にthenの引数にresolveでreturnされた値が代入されることになります。

この場合では、getGitUsername().then(username)=>の部分で、returnで帰ってきたPromiseオブジェクトに対してthenを実行し、resolve(json.login)json.loginがthenの引数であるusernameに代入されます。

今回のコードでは示していませんが、getGitUsername.then.catch(reject => console.log(reject))のようにすると、Promiseのコンストラクタの中でrejectが呼ばれたときに、Promiseがrejectの状態になり、catch内の処理が実行されます(catchはPromiseがrejectの状態のときに呼ばれるため)。

Promiseはresoveとrejectとpendingの三つの状態を持ちます。returnしたPromiseオブジェクトに対してthenを実行すると、Promiseオブジェクトがresolveの状態になるまで待ってからthen以降の処理が実行されます。

また、catchを実行するとPromiseオブジェクトがrejectの状態になるまで待ってからcatch以降の処理が実行されます。

また、このthenやcatchの処理は非同期処理になるため、resolveやrejectが返される前にその次のコードが実行され、resolveやrejectが返ってきて初めて実行されます。

async/awaitを使って非同期処理を制御

今度は、async/awaitを使って非同期処理を制御してみましょう。

こちらの記事を参考にしました。

Promiseが分かれば簡単!async, await

以下のコードです。

import fetch from 'node-fetch';

const getGitUsername = async () => {
    const message = 'GitのユーザーIDは';
    const url = 'https://api.github.com/users/hangi4343'

    const json = await fetch(url)
        .then(res => {

            console.log('非同期処理成功')
            return res.json()
        }).catch(error => {
            console.error('非同期処理失敗', error)
            return null
        })
    console.log(message + json.login)
}
getGitUsername()
console.log('end')

end
非同期処理成功
GitのユーザーIDはhangi4343

それではコードの解説です。

asyncは、関数の前に書きます。このasyncにより、その関数の中でawaitが使えるようになります。

awaitは端的にいうと、非同期処理を同期処理のように書けるようにしてくれるものという風に説明されます。

今回はconst json = await fetch(url)の部分でawaitを使っています。awaitにより、fetch(url)の部分で値がreturnするまで待つことができます。

そのため、値が返ってきたあとにconsole.log(message + json.login)の部分のコードが実行されます。

しかし、awaitはあくまでPromiseを簡単に扱えるようにしたものであり、内部的にはawaitの右側の式を同期的に実行した後、resolveしたpromiseを生成し、awaitの左側および下のコード全てをthenの引数の関数として実行しています。

つまり、この場合においてはconsole.log(message + json.login)の部分がPromiseにおけるthenの後に実行されるものであり、console.log('end')の部分はthenの後の処理ではありません。

つまり、getGitUsername()の実行の完了を待たずに、console.log('end')は実行されます。

終わりに

今回の記事はここまでになります。

お疲れさまでした。