読者です 読者をやめる 読者になる 読者になる

JS.next

JavaScriptの最新実装情報を追うブログ

Promiseについて

★★★ ES2015 仕様紹介

概要

Promiseとは非同期処理を上手く扱う為のAPIであり、パターンである。
非同期の処理の完了後に続けて処理を行いたいとき、よくコールバックパターンが使われるが、処理が連続するとコールバック地獄と言われる分かりづらいソースコードになってしまう。
また、複数の非同期処理が完了した時に処理を行うなど、コールバックパターンでは難しい事をスマートにできるのがこのPromiseである。
今まではDOMの方でDOM Promiseとして仕様策定が進められていたり、ライブラリのDeferredが有名だったが、ES2015標準に入ることになり、V8に実装された。


実装されたメソッド

  • Promise.resolve(x)
  • Promise.reject(x)
  • Promise.all( [p1, p2, p3, ......] )
  • Promise.race( [p1, p2, p3, ......] )
  • Promise.prototype.then(onFulfilled, onRejected)
  • Promise.prototype.catch(onRejected)


使う前に

使うにあたってまず、ES6(2015) Promiseと今までのDOM Promise実装やライブラリのDeferredは、よく似ているけれどいくらか仕様が違うということを注意しなければならない。
ただし概念部分は共通しているので、それらの解説でも十分参考にはできる。
当記事が分かりにくいと感じたら記事末の参考外部リンクを参考にして欲しい。
Promiseはとてもシンプルな仕組みなので、実際に何度も使えばすぐ感覚が掴めてくると思う。


使い方

まず、Promiseコンストラクタに関数を渡して呼び出して、promiseオブジェクトを作る。
作ったpromiseは最初「未解決」状態になり、渡した関数は第一引数に処理が完了した時に呼ぶresolve関数、第二引数に処理が失敗した時に呼ぶreject関数が与えられてすぐに呼ばれる。
resolve関数が呼ばれた時、そのpromiseは解決して「解決済」状態となる。

p1 = new Promise(function (resolve, reject) {
    setTimeout(resolve, 1000)  // 1秒後にこのpromiseを解決する
})

promiseはthenメソッドを持つ。
このメソッドに関数を渡すと、promiseが解決していれば間もなくその関数が呼ばれ、未解決ならば解決された後に呼んでくれる。
つまり、完了状態を気にせず、その処理が終わった後にしたい処理を指定できる。

p1.then(function () {
    alert('1秒経過した')  // resolveが既に呼ばれている場合か、呼ばれた時に実行される
})


値を与えてresolveを呼ぶと、その後のthenで受け取れる。
ちなみにresolveを何度も呼んでも、有効なのは最初の1回のみである。

p2 = new Promise(function (resolve) {
    setTimeout(resolve, 2000, '2秒経過した')
})

p2.then(function (message) {
    alert(message)  // 『2秒経過した』
})

また、エラーが発生した時や、rejectが呼ばれた時はpromiseは「棄却済」状態になる。
「棄却済」になった時の値(エラー)はcatchメソッドで登録した関数まで伝えられる。

p3 = new Promise(function (resolve, reject) {
    reject('エラー') // promiseを棄却する
})

p3.then(function (message) {
    alert(message)  // (呼ばれない)
})

p3.catch(function (error) {
    alert(error) // 『エラー』
})

一度promiseが棄却されると、次のcatchまでその後のthenはスルーされる。
つまり、どこでエラーが起きても、ちゃんとその後の最初のcatchで処理できる。

p4 = new Promise(function (resolve, reject) { reject('エラー') }) 
p4.then(alert).then(alert).then(alert).catch(alert)  // catchのalertだけ呼ばれる

thenの第二引数を使ってcatchの分もまとめて指定することもできる。

p5 = new Promise(function (resolve, reject) {
    reject('エラー') // promiseを棄却する
})

p5.then(function (message) {
    alert(message)  // (呼ばれない)
}, function (error) {
    alert(error) // 『エラー』
})


Promise.resolveを使えば、promise以外の値をその値をもって解決されたpromiseを作れる。
Promise.rejectを使えば、その値をもって棄却されたpromiseを作れる。

Promise.resolve(123).then(alert)   // 『123』

Promise.reject(123).catch(alert)   // 『123』


  // resolveはpromiseが渡された時には再ラップしない
Promise.resolve(Promise.resolve(123)).then(alert)  // 『123』(上と同じ)


thenやcatch中にpromiseが返されるとそのpromiseが以後のthenやcatchの対象になる。
promiseでない値が返された場合はpromiseにキャストされて扱われる。

function delay(s) {
    var p = new Promise(function (resolve) {
        setTimeout(resolve, s*1000, s)
    })
    return p  // 「s秒後に数値sをもって解決するpromise」を返す
}

function log(s) {
    console.log(s+'秒経過した')
    return s  // 「数値sをもって解決されたpromise」にキャストされる
}

delay(3).then(log).then(delay).then(log).then(delay).then(log)
  // 3秒間隔で3回ログが出る

またthenでは、ライブラリで提供されているような、「then」という関数を持っている『thenable』オブジェクトをネイティブPromiseのように扱ってくれるので、他のPromise実装と容易に混用ができる。


複数の処理が全て終わった所から続けたい場合はPromise.allを、いずれかが終わった後に続けたい場合はPromise.raceを使う。

pd1 = delay(1), pd2 = delay(2), pd3 = delay(3)

  // 全てのpromiseが解決したときに、全ての結果の配列と共に呼ばれる
Promise.all([pd1, pd2, pd3]).then(function (d) {
    console.log(d)  // [1, 2, 3]
})
pd1 = delay(1), pd2 = delay(2), pd3 = delay(3)

  // 最初のpromiseが解決したときに、その結果と共に呼ばれる
Promise.race([pd1, pd2, pd3]).then(function (d) {
    console.log(d)  // 1
})


利用例

ajaxでデータを取得して表示する

function ajax(url) {
    return new Promise(function (resolve, reject) {
        var xhr = new XMLHttpRequest
        xhr.open('GET', url)
        xhr.onload = function () {
            if (xhr.status == 200) {
                resolve(xhr.response) // 成功時
            } else {
                reject(new Error(xhr.statusText))  // 404エラーなど
            } 
        }
        xhr.onerror = reject // urlに問題がある場合など
        xhr.send()
    })
}

function onFulfilled(response) { elem.textContent = response }
function onRejected(error)     { elem.textContent = error    }

ajax(url).then(onFulfilled, onRejected)

ajaxで取得したデータをurlに見立てて、再度ajaxでデータを取得する(x3)

ajax(url).then(ajax).then(ajax).then(ajax).then(onFulfilled, onRejected)
  // 途中でどんなエラーが起きてもonRejectedで処理される


実装されたバージョン

  • V8 : 3.23.14(バグ) 3.24.14(修正) 3.25.17(cast→resolve) (3.25.30) 3.26.31.8(デフォルト有効) 4.9.66(非標準メソッドの削除)
  • Chrome : (DOM Promise)~M35, (ES6(2015) Promise)M36(デフォルト有効)
  • Node : 0.11.13(デフォルト有効)


参考外部リンク