JavaScript
promise

Promiseの実装をしっかり読んでみたので学習メモ

More than 3 years have passed since last update.

@armorik83です。ちょっと一段落ついたところで、そろそろ真剣にPromiseの中身を読む必要があるなーと感じたので、そのときのメモです。


読んだ実装

今回はjakearchibald/es6-promise v2.0.1を読んでいきます。基礎知識としてJavaScript Promiseの本にも目を通しておくとよいでしょう。


サンプルソース

サンプルとしてPromise本の1.3.1を若干改変して利用させてもらいました。

var Promise = require('es6-promise').Promise;

function getURL(URL) {
return new Promise(function (resolve, reject) {
var req = new XMLHttpRequest();
req.open('GET', URL, true);
req.onload = function () {
if (req.status === 200) {
resolve(req.responseText);
} else {
reject(new Error(req.statusText));
}
};
req.onerror = function () {
reject(new Error(req.statusText));
};
req.send();
});
}

var URL = "http://httpbin.org/get";
var promise = getURL(URL);

promise.then(function(value){
console.log(value);
}).catch(function(error){
console.error(error);
});


処理の流れ

非同期処理が完了する前にthen()が実行された場合の流れ。非同期処理完了後にthen()する場合は多少前後しますが、stateとかで分岐してうまい具合にやってるみたいです。(今回は割愛)


new Promise()


エントリ

promise.js



  • L136 Promise constructorが呼ばれる


  • L137 L137-L140で各プロパティ初期化


  • L142 new Promise()に与えられたcallbackがnoopでなければ処理


  • L143 各種ガード節(callbackは関数か? ちゃんとnew付けて呼んでるか?)


callbackを実行

-internal.js



  • L226ここからinitializePromise()


  • L228new Promise(ここ)に渡されたcallbackが実行される

callback自体はここでもう完了する。


then()


promise.then()の流れ

promise.js



  • L356 ここからPromise#then()


  • L360 判定式state === FULFILLED && !onFulfillment || state === REJECTED && !onRejection、チェーン.catch()を捌くための分岐


  • L364 .then()チェーン用の新しいPromiseインスタンスchildを生成


  • L142 child生成時、new Promise()に与えられるcallbackはnoopなのでスキップ


  • #365 result初期化時のままundefined


  • #372 stateundefined (=== PENDING)なので、subscribe()に進む


subscribe()

-internal.js



  • L142 subscribers Arrayにchild Promise、onFulfillment callback(.then()の第一引数)、onRejection callback(.then()の第二引数)が格納される


  • L148 parent._statePENDINGのためスキップ

promise.js



  • L381 subscribe()が完了し、then()childを返して完了


catch()

promise.js


promise.then()の流れ(再び)

promise.js


subscribe()(再び)

-internal.js

ここでまだ非同期処理が完了していない場合、一連の処理は一旦ここで終わる。


resolve()


fulfill()

-internal.js



  • L228 非同期処理内でresolve()が呼ばれると、最初にnew Promise()で生成したPromiseインスタンス内でresolve処理が動き出す


  • L098 resolveの成否判定に移る


  • L099 resolve()にPromise自身が渡ってきたら例外、thenableオブジェクトだったらそっち向けの処理(今回は割愛、Promise本2.1.2を参照)


  • L116 fulfill()に移り、このとき引数のvalueにはresolve()に与えた値、つまり非同期処理の結果が渡る


  • L117 stateがPENDINGでなければスキップ(今回はPENDINGなので続行)


  • L120 stateがFULFILLEDに切り替わる


  • L124 subscribersには事前に格納されたchild, onFulfillment, onRejectionがあるためasap()に進む。asap()にはpublish関数とpromise自身が渡る。


publish()

asap.js



  • L003 queueにpublish関数とpromiseが格納される

-internal.js



  • L151 queueが発火してpublish()が走る


  • L159 subscribers Arrayを3ずつイテレートして取り出したchild, onFulfillment, onRejectionを用いてinvokeCallback()する


  • L166 childが無ければ(チェーンが続いてなければ).then() callbackの引数にresolveの結果を渡して実行


  • L188 invokeCallback()から結果に応じてチェーンをresolve()させたりfulfill(), reject()させるなど次々と回収

全行程完了。


要約

逐一書いていったので要約します。大きく分けて3段階となっています。

1


  • new Promise()

  • 渡したcallbackを実行


  • promiseが返る

2



  • .then()する

  • チェーン用のchildを生成


  • onFulfillmentsubscribersに追加される

  • チェーンならば順にchildsubscribersに追加

3


  • 非同期処理内でresolve()する


  • invokeCallback()onFulfillment(result)を実行

  • 順次チェーンの回収


他のPromise実装は?

(追記)もしかするとes6-promiseに限った話なのではと思い、他の有名どころもサラっとですが読んでみました。

bluebird 2.9.13

then/promise 6.1.0

native-promise-only 0.7.6-a

yahoo/ypromise 0.3.0

then()でチェーン用にchild promiseを生成して、queueに追加して発火を待つ、という仕組みはPromise一般論といってよさそうです。


得たもの


  • 複雑そうというPromiseアレルギーの克服

  • キューへの追加と回収に関する知見

  • おそらくキュー実行管理をしているであろうasap.js#のブラウザ、WebWorker、node向け分岐処理の書き方


  • MutationObserverとかいう(残念ながら)聞いたことのなかったAPIを知るきっかけ


不明点

asap.js何やってたのか結局よく分かんない。


今後の課題

もっと色々実装読んでいきたいです。


今回使ったもの

ありがとうございました。