search
LoginSignup
518

More than 3 years have passed since last update.

posted at

updated at

Promise と async/await の理解度をもう1段階上げる

はじめに

表題のモチベーションで書き上げた備忘録に加筆・修正したものを記事にしました。
記事を読んで下さった方の Promise, async/await の理解度が 1LV でもアップしてくれたら嬉しく思います。

Promise とは

  • Promise は非同期関数を扱うためのインターフェース
  • Promise は 悲運のピラミッド型コールバック (ネスト地獄)を根本的に解決してくれる
  • thenable と呼ばれる then メソッドを有するオブジェクトを resolve の第一引数に入れることで Promise オブジェクトへ変換することが可能
// thenable を Promise オブジェクトへ変換する
const converted = Promise.resolve({
  then: (onFulfilled) => onFulfilled('be promise')
})
converted.then(value => console.log(value)) // be promise

async/await とは

  • async/await は Promise のシンタックスシュガーとして用いられる
  • async/await は Promise を同期的なコードのように書ける

Promise の書き方とメソッドの簡単な説明

Promise の最も基本的なコード

const promise = new Promise((resolve, reject) => resolve('解決した'))
const unResolvedPromise = new Promise((resolve, reject) => reject('エラー発生'))

promise
  .then((value) => value) // 解決した

unResolvedPromise
  .then((value) => value) 
  .catch((errorMessage) => new Error(errorMessage)) // error: エラー発生
  .then((value) => value) // エラー発生

then と catch

then と catch はプロミスにおける最重要事項の一つです。これらは promise を返すため、メソッドチェーンを繋げることができます。

then

Promise#then(onFulfilled, onRejected)

  • Promise に成功ハンドラ(onFulfilled) と失敗ハンドラ(onRejected) を付加する
  • 2つの引数を持ち、それらはそれぞれ Promise の状態が Fulfilled の時と Rejected の時のコールバック関数である
  • 返り値は promise

onFulfilled(value)

  • 一つの引数 value を持つ
  • value は promise にラップされた値など

onRejected(reason)

  • 一つの引数 reason を持つ
  • reason は rejected となった理由など

catch

Promise#catch(onRejected)

  • then(undefined, onRejected) のショートカットとして機能する
  • 返り値は promise
  • 引数の onRejected は then と参照

静的メソッド

Promise.resolve

下記コードのエイリアスです。渡した値で Fulfilled される promise オブジェクトを返します。

new Promise((resolve) => resolve(/* value */))

Promise.reject

下記コードのエイリアスです。

new Promise((resolve, reject) => reject(/* reason */))

Promise.all

  • 引数に Promise 関数の配列を取る実行
  • 返り値は Promise
  • 引数に取った Promise で一つでもエラーが発生した場合はその rejected されたエラーが返る
  • エラーが発生しなかった場合は「value にそれぞれの resolve の結果が入った配列」の Promise が返る
Promise.all(promises).then(allFulfilled, firstRejected)

Promise.race

  • 引数に Promise 関数の配列を取り実行する
  • 引数にとった Promise 関数のいずれかが fullFilled または rejected になった時点でその Promise を返す
Promise.race(promises).then(firstFulfilled, firstRejected)

Promise の仕様的なところ

async/await をちゃんと使えるようになるには Promise が内部でどう働いているかある程度わかっている必要があります。

Promise は状態をもつ

インスタンス化された promise のオブジェクトには以下の 3 つの状態が存在します。

  1. Fulfilled
  2. Rejected
  3. Pending
  • promise のオブジェクトの状態は不変である
  • 一度 Fulfilled か Rejected になったら Pending には戻らない
  • Fulfilled, Rejected は Settled(落ち着いた状態, 不変の状態)ともいう
  • Pending から Settled になったら状態はもとに戻らない

渡された関数の実行タイミング

  • すぐに実行されるのではなく、マイクロタスクのキューに入れられる
  • このキューは現在のイベントループの終わりに空になる

async/await の書き方

console.log(101) するコードを (1)then catch, (2)async/await の2パターン書きました。
doSomething 関数内でエラーが発生した場合は failureCallback にエラーが渡されてログに出すようにしています。

then と catch を使った場合

const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)

doSomething()
  .then(v => v + 1)
  .then(v => console.log(v)) // 101
  .catch(failureCallback);

async/await を使った場合


// ※ 即時関数で囲っています
const doSomething = () => Promise.resolve(100)
const failureCallback = (e) => console.log(e)

(async () => {
  try {
    let result = await doSomething()
    result = result + 1
    console.log(result) // 101
  } catch (e) {
    failureCallback(e)
  }
})();

サンプルコード集

筆者の場合 Promise, async/await は説明やコードを読んでいるだけではいまいち理解できなかったので、実際にコードを書いて動かして理解を深めました。その過程で特に残しておきたい(忘れた時に後から見たい)と思ったコードを載せております。

古いコールバックを使った API をラップする

setTimeout をラップします。これはコールバックのAPIをラップしたもっともシンプルな例かもしれません。

// setTimeout を Promise にする
const wait = ms => {
  return new Promise((resolve) => setTimeout(resolve, ms))
}
wait(1000).then(() => console.log('logged after 1000ms'))

複数の関数を直列に合成する

const composeAsync = (...funcs) => {
  return x => {
    return funcs.reduce((acc, val) => {
      return acc.then(val)
    }, Promise.resolve(x))
  }
}

// 使用例
const succ = n => n + 1
const multiply = x => y => x * y

const transformData = composeAsync(succ, succ, multiply(3))
transformData(2)
  .then(v => console.log(v)) // 12

async/await と Promise のコードを比較してみる

async function 内で console.log(100)、async function を呼ぶときに console.log(100) するコードで比較してみます。両コードの挙動は全く同じです。
async/await 使った方が圧倒的に書きやすく、またコードを見た時にどういう動きをしそうか、直感的に理解しやすいと思いました。

async/await

const ret100 = () => Promise.resolve(100)

const asyncFunc = async () => {
  let result = await ret100()
  result += await ret100()
  console.log(result) // log.1 200
  return result // Promise を返す
}
asyncFunc()
    .then(value => console.log(value)) // log2. 200

Promise

const ret100 = () => Promise.resolve(100)

const asyncFuncPromise = () => {
  let result = 0
  return ret100()
    .then(value => {
      result += value
      return ret100()
    })
    .then(value => {
      result += value
      console.log(result) // log.1 200
      return result
    })
}

asyncFuncPromise()
  .then(value => console.log(value)) // log.2 200

async/await のエラー処理の記述パターン

下記はサンプルコード内で使用している errorPromise 関数です。

const errorPromise = () => {
  return new Promise((resolve, reject) => {
    try {
      throw new Error('Some error occured')
      resolve('no error')
    } catch (err) {
      reject(err)
    }
  })
}

1. try/catch を使う

const asyncFuncTryCatch = async () => {
  try {
    const result = await errorPromise()
    console.log(result) // unreachable
  } catch (err) {
    console.log(err)
  }
}

asyncFuncTryCatch() // log. Some error occured

2. try/catch を使わない

const asyncFuncNoTryCatch = async () => {
  const result = await errorPromise()
    .catch(err => err)
  console.log(result)
}

asyncFuncNoTryCatch() // Some error occured

3. async function 内でエラーハンドリングしない

const asyncFuncHandleErrorOut = async () => {
  const result = await errorPromise()
  console.log(result) // unreachable
}

// async function を呼ぶ時にエラーハンドリング
asyncFuncHandleErrorOut()
  .catch(err => console.log(err)) // Some error occured

おまけ. Promise のエラーハンドリング

const asyncPromiseFunc = () => {
  return errorPromise()
    .then(value => value)
    .catch(err => err)
}

asyncPromiseFunc() // Some error occured

非同期関数を並列で実行させたいとき

非同期関数を並列で実行させたいときは Promise.all を使用する必要があります。

例えば、下記のようなコードがあり、 getFoodgetDrink を並列で実行し、両関数が完了した後に orderItems を実行したいとします。



const getFood = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000, 'got food')
  })
}

const getDrink = () => {
  return new Promise(resolve => {
    setTimeout(resolve, 1000, 'got a drink')
  })
}

const orderItems = (...items) => {
  return new Promise(resolve => {
    setTimeout(() => {
      items.forEach((msg) => console.log(msg))
    }, 2000)
  })
}

NGパターン

下記コードは getFood > getDrink > orderItems のように直列で実行されてしまいます。

const myOrder = async () => {
  const msg1 = await getFood()
  const msg2 = await getDrink()
  orderItems(msg1, msg2)
}

myOrder()

OKパターン(1)

(getFood, getDrink) > orderItems のように並列で実行します。
NGパターン(1) 同様 getFood > getDrink > orderItems のように直列で実行されます。
@azu さん 指摘して頂きありがとうございます。)

※修正:20180501.1745
改めて確認したところ、(getFood, getDrink) > orderItems のように並列で実行されるようでした。
但し、 OKパターン(2) のように Promise.all を使用するのが一般的のようなので、そちらを使うのがベターと思われます。

 const myOrder = async () => {
  const msg1 = getFood()
  const msg2 = getDrink()
  orderItems(await msg1, await msg2)
}

myOrder()

OKパターン(2)

(getFood, getDrink) > orderItems のように getFoodgetDrink は並列で実行されます。一般的に使われているのはこちらのパターンのようです。

 const myOrder = async () => {
  const msg1 = getFood()
  const msg2 = getDrink()
  const msgs = await Promise.all([msg1, msg2])
  orderItems(...msgs)
}

myOrder()

おわりに

以上です。
async/await にしろ Promise にしろ、ドキュメントや記事(この記事のような)を読むだけでなく、自分で手を動かしトライ&エラーを繰り返すことって大事だなとあらためて思いました(どの技術の習得もそうですが・・)。

コードや文章の指摘、質問も頂けると助かります。
最後までお読み頂きありがとうございました。

参考資料

Next steps

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
518