Make it to make it

いろいろ作ってアウトプットするブログ

Promise再入門(1)

モダンなJavaScriptでは、非同期処理においてpromiseとasync/awaitが主流となっている。

  • Webpackはコードスプリッティングをpromiseによって簡便化している
  • ブラウザにはビルトインファンクションであるfetchが存在し、resultをawaitできる
  • ReactではReact.Suspenseをpromiseで実装している

ここでは従来のcallbackベースのファンクションからpromise, async/awaitまでをサンプルを通してカバーする。

Promiseとは何か?

callbackはファンクション、promiseはオブジェクト

callbackはあるイベントに対してレスポンスするかたちで実行されるファンクション。どんなファンクションでもcallbackになることができ、どんなcallbackでもファンクションになることができる。

promiseはオブジェクトであり、あるイベントがまだ起こっていない状態(pending)を表現する。あるイベントが起きたかどうかに関する情報、起きた場合の出力の情報を保持している。

callbackは引数として渡され、promiseは返却される

callbackは独立して定義され、あるファンクションに引数として渡され、必要なタイミングまで保持され、関連するイベントが起きたタイミングで実行される。

promiseは非同期の処理を開始させるファンクションによって作られて返却される。関連するイベントが起きたとき、その処理は実行結果をpromiseに格納し、promise側でsuccessまたはfailureハンドラを実行できる。

callbackはsuccessとfailureを扱い、promiseは何も扱わない

callbackはある処理が成功したかどうかの情報とともに使用され、成功時と失敗時のハンドラを用意しておかないといけない。

promiseはデフォルトでは何も扱わないが、successまたはfailureハンドラは後々にアタッチされる。

callbackは複数のイベント扱うことができるが、promiseは最大1つ

callbackはそれらを受け取るファンクションによって複数回実行することができる

promiseは一つのイベントだけを表現することができ、それは成功時または失敗時のものである

CallbackをPromiseで書き換える一例

ログを一定時間ごとに出力するサンプルを下記に示す。

Callback

const log = (message = '') => {
  console.log(message)
}

log('When the raining is ')
setTimeout(() => {
  log('blowing ')
  setTimeout(() => {
    log('in your face,')
  }, 1000)
}, 2000)

Promise

const log = (message = '') => {
  console.log(message)
}

// delay promise
const delay = ms => {
  return new Promise(resolve => {
    setTimeout(resolve, ms)
  })
}

async function logMessage() {
  log('When the raining is ')
  await delay(2000)
  log('blowing ')
  await delay(1000)
  log('in your face,')
}

logMessage()

Thenメソッド

Promiseはまだ起きていないイベントの処理結果を表すものである。 処理結果の取得可能時にcallbackファンクションを自分で登録して、promiseのthenメソッドで渡す必要がある。

// Call the first argument once the promise has successfully resolved
promise.then(value => {
    // ...
});

// Call the second argument if the promise is rejected
promise.then(null, error => {
    // ...
});

// You can handle both cases by passing in two functions
promise.then(
    value => { /* ... */ },
    error => { /* ... */ }
);

Rejectionハンドラ

滅多にないと思うが、もしsuccessハンドラでエラーが起きた場合、当然ながらrejectionハンドラには進まない。

ただし、catchを書いている場合はそちらの処理が実行される。

ここは以外な盲点なので、下記にサンプルを含めて載せておく。

const samplePromise = new Promise((resolve, reject) => resolve('Success!'))

samplePromise.then(
  success => {
    console.log('success handler', success)  // これが出力される
    throw new Error('here is an error!')  // これが出力される
  },
  error => {
    console.log('rejection handler', error)
  }
)
const samplePromise = new Promise((resolve, reject) => resolve('Success!'))

samplePromise.then(
  success => {
    console.log('success handler', success)  // これが出力される
    throw new Error('here is an error!')
  },
  error => {
    console.log('rejection handler', error)
  }
)
.catch(error => {
  console.log('catch handler', error)  // これが出力される
})

上記の例ではcatch内でエラーをログするようにしているが、そのようにエラーハンドリングしっかりとしてあげることでデバッグができることを念頭においておく。

const samplePromise = new Promise((resolve, reject) => reject('Failed!'))

samplePromise.then(
  success => {
    console.log('success handler', success)
    throw new Error('here is an error!')
  },
  error => {
    console.log('rejection handler', error)  // これが出力される
  }
)
.catch(error => {
  console.log('catch handler', error)
})