📋

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

2021/07/04に公開

@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何やってたのか結局よく分かんない。

今後の課題

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

今回使ったもの

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

Discussion