Mithril 1.1.0

Promise(executor)


説明

ES6 PromiseのPolyfillです。

Promiseは非同期処理とともに使う機構です。


シグニチャ

promise = new Promise(executor)

引数 必須 説明
executor (Function, Function) -> any Yes Promiseがどういった理由で解決・破棄されるのかを決定する関数
返り値 Promise Promiseを返します

シグニチャの読み方


executor

executor(resolve, reject)

引数 必須 説明
resolve any -> any No 呼び出すとPromiseが解決する関数
reject any -> any No 呼び出すとPromiseが破棄する関数
返り値 返り値は無視されます

シグニチャの読み方


静的メンバー

Promise.resolve

promise = Promise.resolve(value)

引数 必須 説明
value any No 解決したPromsieが提供する値
返り値 Promise 解決してvalueを返すPromise

シグニチャの読み方


Promise.reject

promise = Promise.reject(value)

引数 必須 説明
value any No 破棄される時に通知される値
返り値 Promise valueを破棄理由として渡されるPromise

シグニチャの読み方


'Promise.all

promise = Promise.all(promises)

引数 必須 説明
promises Array<Promise|any> Yes 解決を待つPromiseのリストもし要素がPromiseでなければ、その値に対して即座にPromise.resolveを呼び出すPromiseと等価
返り値 Promise 引数に渡されたすべてのpromisesが解決した時、あるいはどれか1つでも破棄したときに解決されるPromise

シグニチャの読み方


Promise.race

promise = Promise.race(promises)

引数 必須 説明
promises Array<Promise|any> Yes 解決を待つPromiseのリストもし要素がPromiseでなければ、その値に対して即座にPromise.resolveを呼び出すPromiseと等価
返り値 Promise promisesのうちのどれか1つが解決・破棄されたときに解決されるPromise

シグニチャの読み方


インスタンスメンバー

promise.then

nextPromise = promise.then(onFulfilled, onRejected)

引数 必須 説明
onFulfilled any -> (any|Promise) No Promiseが解決されたときに呼び出される関数。この関数の最初の引数はこのPromiseが解決したときに決定する(次のPromiseに渡る)値です。もし関数の返り値がPromiseでなければ、その値はnextPromiseの解決のために使われます。もしPromiseだった場合には、nextPromiseの受け取る結果はその内部のPromiseに依存します。もし関数が例外を投げると、nextPromiseは拒絶され、理由としてその例外が渡されます。onFulfillednullなら無視されます。
onRejected any -> (any|Promise) No Promiseが拒絶されたときに呼び出される関数。この関数の最初の引数はこのPromiseが拒絶したときに決定する(次のPromiseに渡る)値です。もし関数の返り値がPromiseでなければ、その値はnextPromiseの解決のために使われます。もしPromiseだった場合には、nextPromiseの受け取る結果はその内部のPromiseに依存します。もし関数が例外を投げると、nextPromiseは拒絶され、理由としてその例外が渡されます。onRejectednullなら無視されます。
返り値 Promise 現在のPromiseの状態に依存した値を持つPromise

シグニチャの読み方


promise.catch

nextPromise = promise.catch(onRejected)

引数 必須 説明
onRejected any -> (any|Promise) No Promiseが拒絶されたときに呼び出される関数。この関数の最初の引数はこのPromiseが拒絶したときに決定する(次のPromiseに渡る)値です。もし関数の返り値がPromiseでなければ、その値はnextPromiseの解決のために使われます。もしPromiseだった場合には、nextPromiseの受け取る結果はその内部のPromiseに依存します。もし関数が例外を投げると、nextPromiseは拒絶され、理由としてその例外が渡されます。onRejectednullなら無視されます。
返り値 Promise 現在のPromiseの状態に依存した値を持つPromise

シグニチャの読み方


どのように動作するのか

Promiseは将来得られる値を表現するオブジェクトです。

// 1秒後に解決するPromise
var promise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    resolve("hello")
  }, 1000)
})

promise.then(function(value) {
  // 1秒後に "hello" と出力
  console.log(value)
})

Promiseはm.requestなどの非同期APIとともに使うと便利です。

一般的に、非同期APIは実行の長い時間がかかります。通常の関数のreturn文で値を返すのは現実的ではありません。その代わりに、それらのAPIはバックグラウンドで動作するため、その間に他のJavaScriptのコードが実行できます。それらのAPIが完了すると、取得した値をパラメータに乗せて指定された関数を実行するのが良くある形態です。

m.request関数はリモートサーバーに対してHTTPリクエストをを送り、その結果を待つ必要があります。ネットワークのレイテンシーによって数ミリ秒単位の遅延が発生します。


Promiseチェーン

Promiseは繋げてチェーンさせることができます。thenコールバックから値を返すと、次のthenコールバックで引数として受け取れます。これにより、長いコールバックを使ったコードを、小さな単位の複数の関数にリファクタリングできます。

function getUsers() {return m.request("/api/v1/users")}

// 避けるべきコード: god関数のテストが難しい
getUsers().then(function(users) {
  var firstTen = users.slice(0, 9)
  var firstTenNames = firstTen.map(function(user) {return user.firstName + " " + user.lastName})
  alert(firstTenNames)
})

// 望ましいコード: 小さい関数のテストはしやすい
function getFirstTen(items) {return items.slice(0, 9)}
function getUserName(user) {return user.firstName + " " + user.lastName}
function getUserNames(users) {return users.map(getUserName)}

getUsers()
  .then(getFirstTen)
  .then(getUserNames)
  .then(alert)

リファクタリングしたコードでは、getUsers()はPromiseを返しています。それに対して、3つの関数をチェーンさせています。getUsers()が解決されると、getFirstTen関数の最初の引数にユーザーのリストがアサインされて呼ばれます。この関数は10ユーザー分抜き出してリストを返します。getUserNamesは、引数で渡されたユーザーのリストから名前だけを抽出して返します。最終的にユーザー名のリストが表示されます。

最初のオリジナルコードではHTTPリクエストを送信しないと動かせませんし、最後にalert()を使うため、テストが簡単ではありません。

リファクタリングしたバージョンはgetFirstTenが、位置ずれのエラーがないか、getUserNameが姓と名の間にスペースがあるかどうかのテストがしやすくなっています。


Promiseの吸収

Promiseは他のPromiseを吸収します。基本的には、これはthencatchonFulfilledonRejected onFulfilledのコールバックの引数としてPromiseを受け渡すことができないということです。この機能により、ネストされたPromisがフラットになり、管理しやすくなります。

function searchUsers(q) {return m.request("/api/v1/users/search", {data: {q: q}})}
function getUserProjects(id) {return m.request("/api/v1/users/" + id + "/projects")}

// 避けるべきコード: 悪夢のピラミッド
searchUsers("John").then(function(users) {
  getUserProjects(users[0].id).then(function(projects) {
    var titles = projects.map(function(project) {return project.title})
    alert(titles)
  })
})

// 望ましいコード: フラットなフローのコード
function getFirstId(items) {return items[0].id}
function getProjectTitles(projects) {return projects.map(getProjectTitle)}
function getProjectTitle(project) {return project.title}

searchUsers("John")
  .then(getFirstId)
  .then(getUserProjects)
  .then(getProjectTitles)
  .then(alert)

リファクタリングしたコードではgetFirstIdはIDを返します。これはgetUserProjectsの最初の引数として渡されます。順番としては、まず解決するとプロジェクトのリストとなるPromiseを返します。このPromiseは吸収されるため、getProjectTitlesの最初の引数はPromiseではなく、プロジェクトのリストとなります。getProjectTitles関数はタイトルのリストを返し、このリストが最終的に表示されます。


エラーハンドリング

Promiseは適切なハンドラーにエラーを伝搬させることができます。

searchUsers("ジョン")
  .then(getFirstId)
  .then(getUserProjects)
  .then(getProjectTitles)
  .then(alert)
  .catch(function(e) {
    console.log(e)
  })

これは先ほどのコードにエラーハンドリングを追加したものです。searchUsers関数は、例えばネットワークがオフラインの場合に失敗し、エラーを返します。この時は、.thenコールバックが呼ばれることはなく、.catchコールバックが呼ばれてコンソールにエラーが表示されます。

もしgetUserProjects内のリクエストが失敗するとgetProjectTitlesalertも呼ばれません。.catchコールバックは呼ばれてエラーがログに表示されます。

searchUsersに該当する結果がない時は空の配列が帰り、getFirstIdが存在しない配列要素のidプロパティを取得しにいくためにnull参照例外が発生しますが、エラーハンドラーはこれもキャッチできます。

このエラー伝達のセマンティクスにより、try/catchブロックをいろいろなところに挟み込まなくても良くなるため、各関数が小さく、テスト可能になります。


短縮表記

時々、すでに解決する値はあるが、Promiseでこれをラップしたいことがあります。Promise.resolvePromise.rejectを使うと、これが実現できます。

// localStorageから来たリストをサポート
var users = [{id: 1, firstName: "John", lastName: "Doe"}]

// この場合localStorageにデータが有るかどうかで `users` が存在するか決まる
var promise = users ?Promise.resolve(users) : getUsers()
promise
  .then(getFirstTen)
  .then(getUserNames)
  .then(alert)

複数のPromise

複数のHTTPリクエストを並行で投げ、すべてが完了したことを待って何かコードを実行したいことがあります。Promise.allを使うとこれが実現できます。

Promise.all([
  searchUsers("ジョン"),
  searchUsers("メアリー"),
])
.then(function(data) {
  // data[0]には名前がジョンのユーザーが入る
  // data[1]には名前がメアリーのユーザーが入る

  // 返り値は次の値と同じ: [
  //   getUserNames(data[0]),
  //   getUserNames(data[1]),
  // ]
  return data.map(getUserNames)
})
.then(alert)

上記のコードでは2つの検索が平行して行われます。両方が完了するとすべてのユーザー名が表示されます。

上記のサンプルには他のメリットも説明されています。上記で作ったgetUserNames関数を再利用することができます。


なぜコールバックではないのか

コールバックは非同期計算を行うための別のメカニズムです。 onscrollなどのように、複数回実行される前提であればこちらの方が適切でしょう。

しかし、一回のアクションに対して一度しか実行されないのであれば、こちらのほうが有用です。状態が十分に管理されておらず、さまざまなネストの深さのクロージャーがあるような、深い一連のコールバックをもたらす悪夢のピラミッドと呼ばれるコードの臭いをリファクタリングによって効率的に解決できます。

さらに、Promiseを使うとエラーハンドリングのための定型文を減らすことができます。


License: MIT. © Leo Horie.