Promiseの実装をしっかり読んでみたので学習メモ
@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
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
state
もundefined (=== PENDING)
なので、subscribe()
に進む
subscribe()
-internal.js
-
L142
subscribers
Arrayにchild
Promise、onFulfillment
callback(.then()の第一引数)、onRejection
callback(.then()の第二引数)が格納される -
L148
parent._state
がPENDING
のためスキップ
promise.js
-
L381
subscribe()
が完了し、then()
はchild
を返して完了
catch()
promise.js
-
L412
.then().catch()
チェーンのcatch
部分が走る -
L413 即座に
.then()
に処理が回される(つまりただのシンタックスシュガー)
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
を生成 -
onFulfillment
がsubscribers
に追加される - チェーンならば順に
child
のsubscribers
に追加
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
何やってたのか結局よく分かんない。
今後の課題
もっと色々実装読んでいきたいです。
今回使ったもの
ありがとうございました。
Discussion