JavaScriptの非同期処理について②

JavaScriptの非同期処理について① - 暇人のメモの続きです。今回は主にPromiseとasync/awaitを用いた非同期処理について書いていきます。

Promiseを使った非同期処理の実装

promiseの基本

まず初めにPromiseを使った非同期処理の実装方法について見ていきます。PromiseとはES2015より導入された非同期処理の状態と結果を表現するオブジェクトです。Promiseを使った非同期関数の実装では呼び出されるとすぐにPromiseインスタンスを返します。返された瞬間にはPromiseインスタンスの状態はpendingと呼ばれる状態です。これは結果が未確定であることを表します。結果が確定するとsettledという状態となりこれ以上変化しません。非同期処理に成功した状態をfulfilled、失敗した状態をrejectedと呼びます。Promiseインスタンスを作るPromiseコンストラクタは引数にresolve、rejectという2つの関数を取り、それらの関数を呼び出すとそれぞれ上で述べたfulfilled、rejectedとなります。ちなみにreject()ではなくthrowでエラーを投げても状態rejectedに移行させることができます。以下のように状態は遷移します。

function example_async() {
    return new Promise((resolve, reject) => 
        setTimeout(() => {
            try  {
                 resolve(value)
            } catch (err) {
                 reject(err)
            }
       }, 1000)
     )
}
}
Promiseの非同期処理の結果をハンドリングする

Promiseには非同期処理の結果をハンドリングするために3つのメソッドthen()、catch()、finally()が用意されています。これは例外処理にも似ているのがわかると思いますが、使い方も似ています。すべてPromiseインスタンスのメソッドですのでPromiseを使った非同期関数を実行して戻ってきたPromiseインスタンスに対して呼び出します。逆に言うと当たり前ですが、Promiseインスタンスが返ってきていない関数にthen()、catch()、finally()は使えません、

  • then()

まずthenですがこれはPromiseインスタンスの状態がfulfilledまたはrejectedになった時に実行されるコールバックを引数に指定します。どちらも省略可能です。コールバックの第一引数はfulfilledされた時、第二引数はrejectedされた時に呼び出されるコールバック関数です。
Promise.prototype.then() - JavaScript | MDN
先ほどのソースコードにあるexample_async()非同期関数を実行した後にその結果を使って処理を実行するには以下のようになります。

example_async().then(value => {
  // fulfilled
  なんらかの処理
}, reason => {
  // rejected
  なんらかの処理
});

thenもまたPromiseインスタンスを返します。これを利用してthenを連続で呼び出すこともできます。

example_async()
    .then(なんらかの処理)
    .then(なんらかの処理)
    .then(なんらかの処理)
  • catch()

catchはthenのにおける第一引数、filfiledされた時のコールバックを省略したものです。つまりPromiseがrejectされた時に実行されるコールバック関数を指定するメソッドです。先ほどのthen()でrejectedされた時に呼び出される第二引数のコールバックはcatch()で最後にまとめることができます。またcatch()ではもしthen()の第一引数のコールバックの中でエラーがあってもそれを拾うことができます。以下のようになります。

example_async()
    .then(なんらかの処理)
    .then(なんらかの処理)
    .then(なんらかの処理)
    .catch(err => {
      //ここにエラーハンドリングを集約
    }
  • finally()

finally()は例外処理と同じように非同期の成功、失敗に関わらずsettled状態になったら実行されます。finally()も同様にPromiseインスタンスを返します。

example_async()
    .then(なんらかの処理)
    .then(なんらかの処理)
    .then(なんらかの処理)
    .catch(err => {
      //ここにエラーハンドリングを集約
    }
    .finally(コールバック関数)
// または以下のようにthen()、catch()がなくても呼び出せる
example_async()
    .finally(コールバック関数)
複数の非同期処理の並行実行

先ほどまでは非同期処理を実行した後にその結果を使って別の非同期処理を実行する方法を書いてきました。しかし複数の非同期処理を並行して実行しても問題がない場合もあると思います。そういう時に便利なメソッドとしては4つのAPIが提供されています。

  • Promise.all()
  • Promise.race
  • Promise.allSettled()
  • Promise.any()

どれも似ているので今回はPromise.all()メソッドについてのみ書いていきます。このメソッドは引数にPromiseインスタンスの配列を渡して、すべてがfulfilledになった時にfulfilledになり、一つでもrejectedになったらrejectedになるメソッドです。

イテレータとジェネレータ

ジェネレータを理解するためにまずはイテレータを理解する必要があります。イテレータとは簡単に言うとイテレータリザルトをnext()メソッドで順々に取り出すことができるオブジェクトです。以下の記事が分かりやすいです。
めちゃくちゃ簡素なJavaScriptのイテレータとジェネレータ説明
JavaScript の イテレータ を極める! - Qiita
ジェネレータとは、ジェネレータ関数から返されるオブジェクトです。ジェネレータ関数は途中で処理を止めることができる特殊な関数です。ジェネレータ関数には以下の2つの特徴があります。

  • functionのあとに「*」を付ける。
  • yieldで処理を一時停止することができます。
function* example_generator() {
    yield 1
    yield 2
}
// ジェネレータの生成-①
const generator = example_generator() 

ジェネレータ関数は実行するとジェネレータを生成しますが、その時点では処理は実行されません。上のコードでいう①では処理をしないということです。ジェネレータはnext()メソッドを使うことで最初のyieldまで処理を実行します。そしてそこで処理を一時停止します。次のyieldまで実行するにはもう一度next()メソッドを実行します。イテレータと同じです。これはイテレータプロトコルという仕様に従っています。next()メソッドで返ってくる値はvalueとdoneというキーをもつオブジェクトです。valueはyieldで返す値であり、doneは関数の最後まで行ったかを示す真偽値です。

// ジェネレータの生成
const generator = example_generator() 
// 最初のyieldまで実行  戻ってくるのは {value:1, done: false}
generator.next()
// 次ののyieldまで実行  戻ってくるのは {value:1, done: false}
generator.next()
// 最後まで実行  戻ってくるのは {value:undefined, done: true}
generator.next()

next()メソッドには引数を渡すことができます。個人的にはこのあたりの挙動が少しわかりづらかったです。結論から言うとnext()メソッドの引数は直前に実行したyieldの戻り値になります。またthrowメソッドで直前のyieldでエラーを投げることができます。

function* example_generator2() {
    let a = yield 1
    let b = yield 2
}
// ジェネレータの生成
const generator2 = example_generator2() 
generator2.next()
// 引数「テスト」は変数aに渡される
generator2.next("テスト")
// 引数「テスト2」は変数bに渡される
generator.next("テスト2")
ジェネレータを使ったPromiseの拡張

Promiseとジェネレータを使うと非同期処理を同期処理のようにシンプルに実装できます。以下のようになります。

function* example_generator_async() {
    try {
        const result = yield 非同期関数()
        console.log(result)
    } catch (err) {
        console.log(err);
    }
}
// ジェネレータの生成
const generator = example_generator_async() 
// 非同期関数を実行
const promise = generator.next().value;
//非同期関数の結果をthenに渡して、nextの引数に渡すことで直前のyieldの戻り値とする。
promise.then(result => generator.next(result))

上記のように実装するのは面倒くさいですが、繰り返しnext()メソッドを実行するヘルパー関数を作ればいくらか楽になります。しかしこれらの同様のやり方をシンプルに実装できる方法がすでに導入されています。それが次に紹介するasync/await構文です。

async/awaitを使った非同期処理の実装

async/await構文はPromise+ジェネレータでやっていたことをより簡潔にしたものです。早速ですがコードを見てください、これまでの内容が分かっていればわりとすんなり入ってくると思います。ここで非同期関数()は必ずPromiseインスタンスを返すことに注意してください。

async function example_async_await() {
    try {
        const result = await 非同期関数()
        console.log(result)
    } catch (err) {
        console.log(err);
    }
}

ジェネレータとの違いは「*」がなくなった代わりにfunctionの前に「async」というキーワードがついたことと「yield」が「await」に変わったことです。またnext()を呼び出すヘルパー関数も必要ありません。awaitの後の非同期関数はPromiseインスタンスが返されれば良いのでPromiseの時と同じようにPromise.all()を指定することで並行実行することもできます。

参考:ハンズオン Node.js オライリー・ジャパン 2020/11/13
www.amazon.co.jp