LoginSignup
11
8

More than 1 year has passed since last update.

Promiseの実装を読んでみた

Last updated at Posted at 2021-08-04

Promiseがどのように動くのかが気になったため、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")
  }, 5000)

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

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

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

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

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

コンストラクタの実行が終了したので、thenメソッドのチェーンが実行されます。
thenメソッドの戻り値がPromiseインスタンスなので、thenメソッドチェーンが可能になっています。

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;
    }
  }
}

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) {
  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);
    }
  });
}
11
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
8