js.svg

【JavaScript】Promiseによる非同期処理

JavaScript

非同期処理

非同期処理とは

非同期処理の前に、同期処理について簡単に説明しておきます。 まずは以下の例を見てください。

const print1 = () => {
  console.log('start')
  console.log('Hello World!')
  console.log('end')
}
実行結果
start
Hello World!
end

同期処理は、上記のようにコードの順に処理を実行することです。 そのため、1 つ 1 つの処理の終了を待たなければいけません。

では次の例はどうでしょう。

const print2() = () => {
  console.log('start')
  setTimeout(() => console.log('Hello World!'), 3000)
  console.log('end')
}

setTimeout()は、引数に一定時間後に実行したい処理を指定します。 この例の場合、3 秒後に Hello World!と表示するという処理になります。

これを実行すると、最初の例とは違う結果になります。

実行結果
start
end
Hello World!

この処理のイメージは以下のようになります。

タイミングprint2()setTimeout()
1処理開始
2console.log('start')
3setTimeout()処理開始
4console.log('end')3 秒待機
5console.log('Hello World')

console.log('end')は、setTimeout()の結果を待たずして実行されています。 この間、2 つの処理は同時に実行されます。 このような処理の実行方法を非同期処理といいます。

なぜ非同期処理が必要かというと、同期処理の場合は処理の関連性に関係なく、1 つ 1 つ順に終わらせなければいけないため時間がかかってしまいます。 処理に関連性がないなら別々で実行したほうが早いのは明白です。 2 つのりんごを片手で順に取るより両手で 2 つ取ったほうが早いよね、ということです。

コールバック関数

コールバック関数とは、関数内から実行される関数のことになります。 以下の例の場合、funcBfuncAで実行されているためコールバック関数となります。

const funcA = (f) => {
  console.log('Do funcA')
  f()
}

const funcB = () => {
  console.log('Do funcB')
}

funcA(funcB)

非同期の処理には、このコールバック関数が用いられます。 先程のsetTimeoutに指定した処理もコールバック関数になります。

Promise

Promise とは

Promiseとは、非同期処理が完了(成功または失敗)した後の処理を定義するためのものです。

Promiseの代表的な例として、Fetch APIがあります。 これは API などにリクエストを送信し、データを取得するためのものです。 fetchは戻り値としてPromiseを返します。

fetch('https://hoge.com/fuga')
  .then((response) => {
    //成功時の処理
  })
  .catch((error) => {
    //エラー時の処理
  })
  .finally(() => {
    //最後に必ず実行する処理
  })

Promiseには、待機、成功、失敗の 3 つの状態があります。 上例では、https://hoge.com/fugaへリクエストを送信し、レスポンスが返ってくるまでが待機状態、 正常レスポンスが返ると成功状態、エラーレスポンスが返ると失敗状態となります。

成功時の処理はthenのコールバック関数として指定し、失敗時の処理はcatchのコールバック関数として指定します。 thenのコールバックの引数には処理の結果、fetchの場合はレスポンス情報になります。 catchのコールバックの引数はエラー情報となります。

また、成功と失敗に関係なく、最後に実行したい処理としてfinallyを指定することもできます。

Promise の定義

Promiseを使用することで、簡単に非同期処理を作ることができます。 以下の例を見て下さい。

asyncFunc = (arg) => {
  return new Promise((resolve, reject) => {
    if (arg === 0) {
      resolve('success')
    } else {
      reject('failed')
    }
  })
}
実行結果
asyncFunc(0) //successと表示される
  .then(result => console.log(result))
  .catch(error => console.log(error))

asyncFunc(1) //failedと表示される
  .then(result => console.log(result))
  .catch(error => console.log(error))

非同期処理の関数を作成する場合、戻り値としてPromiseを返すようにします。 Promiseは、new(コンストラクタ)を使ってインスタンスを生成しますが、その引数に非同期で実行したい処理を指定します。 ここ重要なのが、引数のresolverejectです。

resolveは処理の成功を意味し、これを指定することでthenへと繋がります。 引数に指定した値(オブジェクト)は、thenの引数に設定されます。

rejectは処理の失敗を意味し、これを指定することでcatchへと繋がります。 引数に指定した値(オブジェクト)は、catchの引数に設定されます。

Promise チェーン

非同期処理asyncFunc1()の結果を受けて、非同期処理asyncFunc2()を実行するにはどうすればよいでしょうか。 最初に思いつくのは、以下のようなネストによる定義方法です。

asyncFunc1()
  .then((result1) => {
    console.log('success1')
    asyncFunc2()
      .then((result2) => {
        console.log('success2')
      })
      .catch((error2) => {})
  })
  .catch((error1) => {})

しかし、これでは処理が増えるとともにネストが増えてしまい、コードの可読性を損ないます。

これを解決するために、Promise チェーンというものがあります。 これは、戻り値にPromiseを指定することで、次のPromiseの処理を実行するというものです。 上記の例を Promise チェーンで定義すると以下のようになります。

asyncFunc1()
  .then((result) => {
    console.log('success1')
    return asyncFunc2()
  })
  .then((result) => {
    console.log('success2')
  })
  .catch((error) => {})

Promise チェーンの場合、catchは 1 つで済みます。 ただし、errorにはそれぞれのエラー情報が設定されることに注意してください。

並列処理

Promiseの処理は、Promise.all()を使用することによって並列に実行することができます。 並列に実行する処理を配列で定義し、その結果は配列で返されます。

const promises = [asyncFunc1(), asyncFunc2(), asyncFunc3()]

Promise.all(promises)
  .then((results) => {
    console.log(results[0]) //asyncFunc1()の結果
    console.log(results[1]) //asyncFunc2()の結果
    console.log(results[2]) //asyncFunc3()の結果
  })
  .catch((error) => {})

1 つでもエラーが発生した場合は、全体のエラーとしてcatchが実行されます。 catchの引数には、一番最初にエラーとなった処理の情報が設定されます。

async/await

async/await とは

async/awaitは、Promiseによる同期処理をより簡潔に定義できることを目的にした仕組みです。 詳細は後述しますが、Promisethenが不要になるため、通常のプログラム同様に同期処理が定義できるようになります。

また非同期処理自体の定義も簡潔にできるようになります。

async 関数

関数にasyncを付与することで、Promiseを返す非同期処理にすることができます。 Promiseの定義で例に出したasyncFuncasyncを用いて定義すると以下のようになります。

const asyncFunc = async (arg) => {
  if (arg === 0) {
    return 'success'
  } else {
    throw 'failed'
  }
}

returnresolveに該当し、throwrejectに該当します。 Promiseの定義よりもスッキリとしたのがわかります。

await

awaitは非同期の処理に付与することで、その非同期処理を同期的にすることができます。 以下の例を見てください。

const doProcess = async () => {
  console.log('start')
  const result = await asyncFunc(0).catch((err) => {
    console.log(err)
  })
  console.log(result)
  console.log('end')
}
実行結果
start
success
end

awaitを付与することで、asyncFunc()の処理が終了を待ってから、console.log('end')が実行されていることがわかります。 つまり、awaitより後に書かれた処理は、Promisethenで定義された処理とイコールになります。

またawaitを付与すると、戻り値にはthenの引数に設定されていた値が返されます。

awaitは、async関数内でのみ使用できるので注意が必要です。