Tommy's Blog

Promiseの内部実装を読む

こんにちは。今回はPromiseの実装(参考レポジトリ)を読んでみた話です。

(※1 全てをくまなく読んだというわけではなく、あるユースケースのときに内部ではどのように動くのかを調べました)
(※2 本記事で引用しているPromiseのコードにおいて、ユースケースに関係のないところは端折っています)

ケース1 同期処理をラップしたとき

以下のような例を考えます。

new Promise(resolve => {
  const res = doSomethingSynchronously()
  resolve(res)
}).then(value => {
  console.log(value)
}

console.log("main")

このとき、

  1. コンストラクタに渡した関数が実行される
  2. thenメソッドによってコールバックが登録される
  3. console.log("main")が実行される
  4. コールバックが実行される

という順番で処理が行われます。なぜそうなるかを追っていきます。

Step1 Promiseのコンストラクタを実行

Promiseのコンストラクタの実装は次のようなコードです。
いくつかの状態を初期化し、doResolve関数を呼び出しています。

function Promise(fn) {
  this._deferredState = 0;
  this._state = 0;
  this._value = null;
  this._deferreds = null;
  if (fn === noop) return;
  doResolve(fn, this);
}

doResolve関数では、指定した関数を実行しています。
fnに与えられている引数が、Promiseインスタンスを作るときによく使うresolve, rejectにあたるものです。

function doResolve(fn, promise) {
  fn(function (value) {
    resolve(promise, value);
  }, function (reason) {
    reject(promise, reason);
  });
}

resolve / rejectの実行

doResolve関数ではresolve関数、 あるいはreject関数が呼ばれているので、その実装を見ていきます。
ここではresolve関数のみを考えます。

resolve関数ではPromiseインスタンスのstateを1に、valueをnewValueに代入しています。
そして、finale関数を実行しています。しかし、Promise.thenがまだ実行されていないので、finale関数を実行しても何も起きません。
よって、ここでPromiseのコンストラクタの一連の実行が終わります。

function resolve(self, newValue) {
  self._state = 1;
  self._value = newValue; // 重要
  finale(self);
}

promise._stateについて

stateはPromiseインスタンスの状態を表すものです。
具体的には、0ならばpending、 1ならばfulfilled、2ならばrejectedとなります。

promise._valueについて

resolve関数において、self._value = newValueという文がありました。
newValueはresolve(value)のvalueにあたります。このvalueはthenメソッドで登録したコールバック関数に渡す必要があります。
しかし、何もしないとコンストラクタの処理が終わった時点で失われてしまいます。そこで、Promiseインスタンス内に一時保存しておいて、必要になったときに取り出すような仕組みになっています。

step2 thenメソッドによるコールバックの登録

次にthenメソッドによってコールバックを登録します。thenメソッドの実装は次のようになっています。

1行目ではPromiseインスタンスを生成しています。noopを渡すことによって、Promiseコンストラクタでは状態の初期化のみが行われるようになります。このインスタンスが戻り値となっていることによって、thenメソッドのチェーンが可能になっています。
2行目ではhandle関数を呼んでいます。deferredというのは、onFulfilled, onRejected, 次のPromiseインスタンスをまとめたインスタンスです。
handle関数内ではhandleResolved関数が呼ばれています。ここで__非同期__に処理を実行します。実行される処理は、Promiseインスタンスから値を取り出し、コールバックの実行をし、最後に次のPromiseのresolveを呼び出します。(thenメソッドチェーンで登録した関数をどんどん呼んでいく)。

ここでasapというのはprocess.nextTick のようなもので、重要な役割を担っています。
すなわち、then(callback)を実行したときにPromiseの状態がfulfilledであっても、すぐにcallbackが呼ばれるわけではないということです。実行のタイミングは他に処理がおこなわれていないときです。

Promise.then = function(onFulfilled, onRejected) {
  var res = new Promise(noop);
  handle(this, new Handler(onFulfilled, onRejected, res));
  return res;
}

function handle(self, deferred) {
  handleResolved(self, deferred);
}

function Handler(onFulfilled, onRejected, promise){
  this.onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : null;
  this.onRejected = typeof onRejected === 'function' ? onRejected : null;
  this.promise = promise;
}

function handleResolved(self, deferred) {
  // process.nextTickのようなもの
  asap(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;

    var ret = cb(self._value)

    if (ret === IS_ERROR) {
      reject(deferred.promise, LAST_ERROR);
    } else {
      resolve(deferred.promise, ret);
    }
  });
}

ケース2 非同期処理をラップ

次に、非同期処理を実行する例を考えます。

new Promise((resolve) => {
  // 非同期処理
  setTimeout(() => {
    resolve("RESOLVED")
  }, 1000000)

}).then(value => {
  return value
}).then(value => {
  console.log(value)
})

step1 Promiseのコンストラクタを実行

Promiseコンストラクタの引数で指定した関数を実行する.今回であれば以下の関数が実行される。

setTimeout(() => {
  resolve(this, "RESOLVED")
}, 1000000)

step1 Promiseのコンストラクタを実行

Promiseコンストラクタの引数で指定した関数を実行します。今回であれば以下の関数が実行されます。

setTimeout(() => {
  resolve(this, "RESOLVED")
}, 5000)

step2 thenメソッドによるコールバックの登録

コンストラクタの実行が終了したので、thenメソッドのチェーンが実行されます。
thenメソッドの戻り値がPromiseインスタンスなので、thenメソッドチェーンが可能になっています。
まだPromiseインスタンスはsuspendな状態なので、コールバックを登録したあとは何も行われません。

Promise.then = function(onFulfilled, onRejected) {
  var res = new Promise(noop);
  handle(this, new Handler(onFulfilled, onRejected, res));
  return res; 
}
function handle(self, deferred) {
  if (self._state === 0) {
    if (self._deferredState === 0) {
      self._deferredState = 1;
      self._deferreds = deferred; // 次のPromiseの格納
      return;
    }
  }
}
function handle(self, deferred) {
  if (self._state === 0) {
    if (self._deferredState === 0) {
      self._deferredState = 1;
      self._deferreds = deferred; // 次のpromiseの格納
      return;
    }
  }
}

/**
 * CURRENT PROMISE <= NEXT PROMISE
 * 
 * /

step3 非同期処理が実行可能になった時

Promiseインスタンス生成時に渡した関数を実行します。
(今回でいえば、resolve(this, "RESOLVED")が最短で5秒後に呼ばれる)

Step2のhandle関数内でpromise.defferedState = 1と設定されていたので、finale関数内のhandle関数が呼ばれます。

そして、hanleResolved関数が呼ばれることによって、非同期に「thenメソッドで登録したコールバックを実行 -> 次のPromiseをresolveする」を行います。

function resolve(self, newValue) {
  self._state = 1;
  self._value = newValue;
  finale(self);
}
function finale(self) {
  if (self._deferredState === 1) {
    handle(self, self._deferreds);
    self._deferreds = null;
  }
}

function handle(self, deferred) {
  // if (self._state === 0) {
  //   // 参考までに
  //   // 同じインスタンスに対して、複数回thenメソッドを呼び出す時とか
  //   if (self._deferredState === 1) {
  //     self._deferredState = 2;
  //     self._deferreds = [...self._deferreds, deferred];
  //     return;
  //   }
  //   self._deferreds.push(deferred);
  //   return;
  // }
  handleResolved(self, deferred);
}
function handleResolved(self, deferred) {
  asap(function() {
    var cb = self._state === 1 ? deferred.onFulfilled : deferred.onRejected;

    var ret = tryCallOne(cb, self._value);
    if (ret === IS_ERROR) {
      reject(deferred.promise, LAST_ERROR);
    } else {
      resolve(deferred.promise, ret);
    }
  });
}