JavaScriptの勉強中:その8 Promise / async await

ハイボール片手に。今日は Promise やるぞー。

bufferings.hatenablog.com

## 13.2 Promises

  • callback だとネストが深くなる→ Promise だとリニアに書ける
  • callback だとエラーハンドリングが難しい→ Promise だとエラー処理の方法が標準化されてる

わかる。

Promiseの用語

  • "fulfillled" - 最初のコールバックが呼び出されるとき
  • "rejected" - 2番目のコールバックが呼び出されるとき
  • "settled" - "fulfilled" か "rejected" になったとき
  • "pending" - まだ "fulfilled" でも "rejected" でもないとき
  • "resolved" - あとで説明する

"resolved" かー、なるほど?(わかってない

Promiseのチェーン

  • then() は新たな Promise オブジェクトを返す(p1 と呼ぶことにする)。この Promise は then() に渡した処理が終わると fulfilled になる。

もう少し詳しく言うと(resolvedの話)

  • then() に渡した処理が Promise 以外の値を返すと、then() の返した Promise (p1) は fulfilled になる
  • then() に渡した処理が Promise を返すと(p2)、then() の返した Promise (p1) は resolved にはなるが fulfilled にはならない
    • p2 が fulfilled になると p2 と同じ値で p1 も fulfilled になる
    • p2 が rejected になると p2 と同じ値で p1 も rejected になる

へー。 resolved と settled が違うんだねー。 Promise が Promise を処理するときに差が出てくる感じだな。

エラーハンドリング

  • .then() の2番目の引数に関数を渡してあげると、処理中に例外が発生したときにその関数が呼び出される
  • けど、実際はそうすることは少ない。 .catch() を使う。 .catch(handleError).then(null, handleError) のショートハンド
  • 例外が発生したら、そこから .catch() があるところまでチェーンを降りていく
  • .catch() の後に処理が続く場合、.catch() が普通に値を返すと、その次の処理にうつる

finally()

  • ES2018
  • コールバック関数は引数を受け取らない
  • .finally() のコールバック関数が返す値は基本的には無視される
  • .finally() で返された Promise は、その前の Promise の結果の resolved または rejected の値を返す
  • ただし .finally() で例外が投げられた場合はその値で rejected になる

へー。1個前の値が渡されるのかー。

Promise.all()

  • を使うと並列に実行することができる
  • Promise の配列を引数として受け取る
  • 戻り値の Promise は、入力値の Promise が1つでも rejected だったら rejected になる
  • 最初に rejected になった Promise が出た時点で、まだ実行中の Promise があっても、すぐに rejected になる
const urls = [ /* URLs */ ];
let promises = urls.map(url => fetch(url).then(r => r.text()));
Promise.all(promises)
    .then(bodies => { /* 処理 */ })
    .catch(e => console.error(e));
  • Promise.all() は実はもうちょっとフレキシブルで、入力値の配列の要素は Promise じゃなくても大丈夫。その場合は fulfilled された Promise のように扱われて、そのまま出力配列にコピーされる

あんまり使わなさそうかなぁとは思うけど、一応片隅に置いとくかな。

Promise.allSettled()

  • ES2020
  • all() のように Promise の配列を受け取る
  • rejected になることはない。全ての Promise の処理が終わるのを待って fulfilled になる
  • 結果はオブジェクトの配列になっていて、各要素に status プロパティがあり "fulfilled" または "rejected" がセットされている
  • "fulfilled" の場合は value プロパティに値が、"rejected" の場合は reason プロパティにエラーまたはリジェクトの値が入っている
> Promise.allSettled([Promise.resolve(1), Promise.reject(2), 3]).then(results => console.log(results));
Promise { <pending> }
> [
  { status: 'fulfilled', value: 1 },
  { status: 'rejected', reason: 2 },
  { status: 'fulfilled', value: 3 }
]

こっちのが all() より扱いやすいね。好きな感じ。

Promise.race()

  • 同時に実行して最初に fulfilled または rejected になったものだけを返す。

Promise を書く

  • 非同期に処理する必要がないものを Promise にして返したい場合は Promise.resolve()Promise.reject() を使えばいい
  • 新たに Promise を生成したい場合は Promise コンストラクターを使う
Promise p = new Promise((resolve, reject) => {
});
  • resolve 関数を使うと resolved または fulfilled になる
  • reject 関数を使うと rejected になる

## 13.3 acync and await

  • ES2017
  • この2つの新しいキーワードのおかげで Promise がすごく簡単に使えるようになって、Promise ベースの非同期のコードが同期のコードみたいに書けるようになった

await

  • await キーワードは Promise を受け取って、それを通常の値または例外に変換する。
let response = await fetch("/api/user/profile");
  • 同期処理のように書けるけど、実際は非同期処理になっている。なので await を使う関数それ自体も非同期な関数じゃないといけない。

try catch 書けるの幸せ。

async

  • await キーワードは async キーワードがついた関数の中でしか使えない、というルール。
async function getHighScore() {
    let response = await fetch("/api/user/profile");
    let profile = await response.json();
    return profile.highScore;
}
  • async な関数の戻り値は Promise になる。通常の値を返していたらそれが resolve された Promise を、例外を投げたらその例外で reject された Promise を返す。

お、あっさり async await が終わってしまった。シンプルでパワフルだなー。

## 13.4 Asynchronous Iteration

for/await ループ

  • ES2018

単に Promise の配列の場合、こんな風に書くことができるけど:

for(const promise of promises) {
    response = await promise;
    handle(response);
}

for/await ループを使うとシンプルにこう書ける:

for await (const response of promises) {
    handle(response);
}

これは普通の Iterator を使った場合で、ちょっとシンプルになるくらいだけど、Asynchronous Iterator の場合はもっと面白い。

Asynchronous Iterator

  • Symbol.iterator じゃなくて Symbol.asyncIterator でメソッドを定義する。
  • next() は Promise を返す。

ちなみに for/awaitSymbol.iterator に対しても動くけど、まず先に Symbol.asyncIterator が定義されていないかを探す。

Asynchronous Generators

  • async function * で Asynchronous Generator を定義できる
  • 内部では await を利用できる
  • yield で返される値は自動的に Promise でラッピングされる
async function* clock() {
    for(let count = 1; count <= 10; count++) {
        await new Promise(resolove => setTimeout(resolove, 100));
        yield count;
    }
}

async function test() {
    for await (let tick of clock()) {
        console.log(tick);
    }
}

test();

今まで勉強してきたのの組み合わせって感じね。

Asynchronous Iterators

Symbol.asyncIterator でメソッドを定義するだけだな。

class Clock {
    async *[Symbol.asyncIterator]() {
        for(let count = 1; count <= 10; count++) {
            await new Promise(resolove => setTimeout(resolove, 100));
            yield count;
        }
    }   
}

async function test() {
    for await (let tick of new Clock()) {
        console.log(tick);
    }
}

また別の例で、AsyncQueue を実装して、enqueue より先に dequeue を呼び出すことができるのとか面白かった。dequeue が unresolved な Promise を返すから。

Async なイテレーションは、実際にそういうのを使いたい場面に出くわしたら、もっとしっくりくるんだろうな。

はい、今日も面白かったー。