Help us understand the problem. What is going on with this article?

Google Input Tools で使われる Promise の実装を読んでみた

More than 3 years have passed since last update.

ES6 で策定された Promise の挙動を Chrome で試してみていて、実装が気になったので chromium のソースコードを読んでみた。
(1/3 追記) コメントでツッコミもらったんですが、以下で読んだ /third_party/google_input_tools/third_party/closure_library/closure/goog/promise/promise.js は Google Input Tools で使うための Promise 実装で、Chrome で動いているものとは別物でした。Chrome で動いてる Promise は、v8 の repository で実装されてるみたいです ( https://chromium.googlesource.com/v8/v8/+/master/src/js/promise.js )

対象となるファイルは、 choromium の /third_party/google_input_tools/third_party/closure_library/closure/goog/promise/promise.js

Promise の生成

  76 goog.Promise = function(resolver, opt_context) {
  77   /**
  78    * The internal state of this Promise. Either PENDING, FULFILLED, REJECTED, or
  79    * BLOCKED.
  80    * @private {goog.Promise.State_}
  81    */
  82   this.state_ = goog.Promise.State_.PENDING;
.
.
.
 161   if (resolver != goog.nullFunction) {
 162     try {
 163       var self = this;
 164       resolver.call(
 165           opt_context,
 166           function(value) {
 167             self.resolve_(goog.Promise.State_.FULFILLED, value);
 168           },
 169           function(reason) {
.
.
.
 185             self.resolve_(goog.Promise.State_.REJECTED, reason);
 186           });
 187     } catch (e) {
 188       this.resolve_(goog.Promise.State_.REJECTED, e);
 189     }
 190   }
 191 };

167行目 , 185行目, 188行目this.resolve_ を呼び出している。 resolve_ の中で、真の resolve の処理を行う(Promise 生成段階ではまだ resolve の処理が登録されてないので、それを抽象化してる)。
state の管理をしていて、これは初期状態だと goog.Promise.State_.PENDING 、resolve か reject が確定すると goog.Promise.State_.FULFILLEDgoog.Promise.State_.REJECTED になる。

 895 goog.Promise.prototype.resolve_ = function(state, x) {
 896   if (this.state_ != goog.Promise.State_.PENDING) {
 897     return;
 898   }
 899
 900   if (this == x) {
 901     state = goog.Promise.State_.REJECTED;
 902     x = new TypeError('Promise cannot resolve to itself');
 903   }
 904
 905   this.state_ = goog.Promise.State_.BLOCKED;
 906   var isThenable = goog.Promise.maybeThen_(
 907       x, this.unblockAndFulfill_, this.unblockAndReject_, this);
 908   if (isThenable) {
 909     return;
 910   }
 911
.
.
.
 923 };

906 行目の maybeThen_ は、valueThenable なオブジェクトの時の処理を行うっぽい。

 926 /**
 927  * Invokes the "then" method of an input value if that value is a Thenable. This
 928  * is a no-op if the value is not thenable.
 929  *
 930  * @param {*} value A potentially thenable value.
 931  * @param {!Function} onFulfilled
 932  * @param {!Function} onRejected
 933  * @param {*} context
 934  * @return {boolean} Whether the input value was thenable.
 935  * @private
 936  */
 937 goog.Promise.maybeThen_ = function(value, onFulfilled, onRejected, context) {
 938   if (value instanceof goog.Promise) {
 939     value.thenVoid(onFulfilled, onRejected, context);
 940     return true;
 941   } else if (goog.Thenable.isImplementedBy(value)) {
 942     value = /** @type {!goog.Thenable} */ (value);
 943     value.then(onFulfilled, onRejected, context);
 944     return true;
 945   } else if (goog.isObject(value)) {
 946     try {
 947       var then = value['then'];
 948       if (goog.isFunction(then)) {
 949         goog.Promise.tryThen_(
 950             value, then, onFulfilled, onRejected, context);
 951         return true;
 952       }
 953     } catch (e) {
 954       onRejected.call(context, e);
 955       return true;
 956     }
 957   }
 958
 959   return false;
 960 };

逆に、Thenable でない時は、 this.resul_x を set してから、this.state_goog.Promise.State_.FULFILLED もしくは goog.Promise.State_.REJECTED に更新。

また、 916行目this.parent_ への参照も切っている(これはGCのため?)
そして、 917行目this.scheduleCallbacks_() でスケジューリング。

 895 goog.Promise.prototype.resolve_ = function(state, x) {
.
.
.
 912   this.result_ = x;
 913   this.state_ = state;
 914   // Since we can no longer be canceled, remove link to parent, so that the
 915   // child promise does not keep the parent promise alive.
 916   this.parent_ = null;
 917   this.scheduleCallbacks_();
 918
 919   if (state == goog.Promise.State_.REJECTED &&
 920       !(x instanceof goog.Promise.CancellationError)) {
 921     goog.Promise.addUnhandledRejection_(this, x);
 922   }
 933 }

scheduleCallbacks_ の実装は以下。this.executeCallbackssync で実行する。

1022 goog.Promise.prototype.scheduleCallbacks_ = function() {
1023   if (!this.executing_) {
1024     this.executing_ = true;
1025     goog.async.run(this.executeCallbacks_, this);
1026   }
1027 };

executeCallbacks_ の実装は以下。popEntry で登録された callback を取り出して、順番に実行する。おそらく、 then でチェーンした callback を順番に実行していくシチュエーション。

1101 goog.Promise.prototype.executeCallbacks_ = function() {
1102   var entry = null;
1103   while (entry = this.popEntry_()) {
1104     if (goog.Promise.LONG_STACK_TRACES) {
1105       this.currentStep_++;
1106     }
1107     this.executeCallback_(entry, this.state_, this.result_);
1108   }
1109   this.executing_ = false;
1110 };

this.popEntry では、chain list になった this.callbackEntries から entry を順番に取り出している。

1061 goog.Promise.prototype.popEntry_ = function() {
1062   var entry = null;
1063   if (this.callbackEntries_) {
1064     entry = this.callbackEntries_;
1065     this.callbackEntries_ = entry.next;
1066     entry.next = null;
1067   }
1068   // It the work queue is empty clear the tail too.
1069   if (!this.callbackEntries_) {
1070     this.callbackEntriesTail_ = null;
1071   }
1072
1073   if (entry != null) {
1074     goog.asserts.assert(entry.onFulfilled != null);
1075   }
1076   return entry;
1077 };

1107行目this.executeCallback_(entry, this.state_, this.result_) で、entry の中身を実行( entry は resolve と reject をどっちも持ってるので、state が必要)。ちなみにこの時点で、何も callback が登録されていない( this.callbackEntries が初期値の null のままだ)と、null が返される。ので、promise を生成しただけの状態だと、 1025行目this.executeCallbacks_ は何もせずに終了するはず。実際、Promise を生成して then を呼び出さないと、何にも起きない。

逆に、promise に対して then を呼び出すたびにちゃんと callback は実行される。これはおそらく、this.result_ で callback 用の値を残している為。つまり、Promise の初期化時に指定した処理はすぐに実行され、その時点で this.value_this.state_ は保存されて、その後は then を呼び出すたびに保存した値が使われる。

  76 goog.Promise = function(resolver, opt_context) {
.
.
.
  97   /**
  98    * The linked list of {@code onFulfilled} and {@code onRejected} callbacks
  99    * added to this Promise by calls to {@code then()}.
 100    * @private {?goog.Promise.CallbackEntry_}
 101    */
 102   this.callbackEntries_ = null;
 103
 104   /**
 105    * The tail of the linked list of {@code onFulfilled} and {@code onRejected}
 106    * callbacks added to this Promise by calls to {@code then()}.
 107    * @private {?goog.Promise.CallbackEntry_}
 108    */
 109   this.callbackEntriesTail_ = null;

おまけで this.executeCallback_ の実装を見ておくと、以下の様になっている。

1124 goog.Promise.prototype.executeCallback_ = function(
1125     callbackEntry, state, result) {
1126   // Cancel an unhandled rejection if the then/thenVoid call had an onRejected.
1127   if (state == goog.Promise.State_.REJECTED &&
1128       callbackEntry.onRejected && !callbackEntry.always) {
1129     this.removeUnhandledRejection_();
1130   }
1131
1132   if (callbackEntry.child) {
1133     // When the parent is settled, the child no longer needs to hold on to it,
1134     // as the parent can no longer be canceled.
1135     callbackEntry.child.parent_ = null;
1136     goog.Promise.invokeCallback_(callbackEntry, state, result);
1137   } else {
1138     // Callbacks created with thenAlways or thenVoid do not have the rejection
1139     // handling code normally set up in the child Promise.
1140     try {
1141       callbackEntry.always ?
1142           callbackEntry.onFulfilled.call(callbackEntry.context) :
1143           goog.Promise.invokeCallback_(callbackEntry, state, result);
1144     } catch (err) {
1145       goog.Promise.handleRejection_.call(null, err);
1146     }
1147   }
1148   goog.Promise.returnEntry_(callbackEntry);
1149 };

Promise.prototype.then の中身を見る

callback の登録のフローを見ていく。

 549 goog.Promise.prototype.then = function(
 550     opt_onFulfilled, opt_onRejected, opt_context) {
 551
 552   if (opt_onFulfilled != null) {
 553     goog.asserts.assertFunction(opt_onFulfilled,
 554         'opt_onFulfilled should be a function.');
 555   }
 556   if (opt_onRejected != null) {
 557     goog.asserts.assertFunction(opt_onRejected,
 558         'opt_onRejected should be a function. Did you pass opt_context ' +
 559         'as the second argument instead of the third?');
 560   }
 561
 562   if (goog.Promise.LONG_STACK_TRACES) {
 563     this.addStackTrace_(new Error('then'));
 564   }
 565
 566   return this.addChildPromise_(
 567       goog.isFunction(opt_onFulfilled) ? opt_onFulfilled : null,
 568       goog.isFunction(opt_onRejected) ? opt_onRejected : null,
 569       opt_context);
 570 };
 571 goog.Thenable.addImplementation(goog.Promise);

then の中ではほぼ何もしてなくて、ただ addChildPromise_ を呼んでいる。

 812 goog.Promise.prototype.addChildPromise_ = function(
 813     onFulfilled, onRejected, opt_context) {
 814
 815   /** @type {goog.Promise.CallbackEntry_} */
 816   var callbackEntry = goog.Promise.getCallbackEntry_(null, null, null);
 817
 818   callbackEntry.child = new goog.Promise(function(resolve, reject) {
 819     // Invoke onFulfilled, or resolve with the parent's value if absent.
 820     callbackEntry.onFulfilled = onFulfilled ? function(value) {
 821       try {
 822         var result = onFulfilled.call(opt_context, value);
 823         resolve(result);
 824       } catch (err) {
 825         reject(err);
 826       }
 827     } : resolve;
 828
 829     // Invoke onRejected, or reject with the parent's reason if absent.
 830     callbackEntry.onRejected = onRejected ? function(reason) {
 831       try {
 832         var result = onRejected.call(opt_context, reason);
 833         if (!goog.isDef(result) &&
 834             reason instanceof goog.Promise.CancellationError) {
 835           // Propagate cancellation to children if no other result is returned.
 836           reject(reason);
 837         } else {
 838           resolve(result);
 839         }
 840       } catch (err) {
 841         reject(err);
 842       }
 843     } : reject;
 844   });
 845
 845
 846   callbackEntry.child.parent_ = this;
 847   this.addCallbackEntry_(callbackEntry);
 848   return callbackEntry.child;
 849 };
  • 1. 空のcallbackEntryを作成
  • 2. callbackEntry.child に、Promiseを設定。この際、この Promise で設定された resolve の 引数には、onFullfilled の返り値を設定する。
  • 3. then を呼び出した promise を callbackEntry.child.parent_ として設定(Promise の親子関係を作る)。
  • 4. this.addCallbackEntry_(callbackEntry) を呼ぶ(ここで callback の追加や実行を行う)
  • 5. callbackEntry.child を返す(これが Promise になってる)

callbackEntryの初期化は getCallbackEntry で行っている。

 300 goog.Promise.getCallbackEntry_ = function(onFulfilled, onRejected, context) {
 301   var entry = goog.Promise.freelist_.get();
 302   entry.onFulfilled = onFulfilled;
 303   entry.onRejected = onRejected;
 304   entry.context = context;
 305   return entry;
 306 };

また、addCallbackEntry_ は以下。ここで 787行目schedleCallbacks_() が呼ばれている為、then を呼ぶたびに callback の処理が実行される。

 783 goog.Promise.prototype.addCallbackEntry_ = function(callbackEntry) {
 784   if (!this.hasEntry_() &&
 785       (this.state_ == goog.Promise.State_.FULFILLED ||
 786        this.state_ == goog.Promise.State_.REJECTED)) {
 787     this.scheduleCallbacks_();
 788   }
 789   this.queueEntry_(callbackEntry);
 790 };

ちなみに、784行目hasEntry_ というのは this.callbackEntries の有無を確かめてるだけっぽい。

1034 goog.Promise.prototype.hasEntry_ = function() {
1035   return !!this.callbackEntries_;
1036 };

また、 789行目queueEntry_ で、chain list のthis.callbackEntries_ に対して callback の追加を行う。

1043 goog.Promise.prototype.queueEntry_ = function(entry) {
1044   goog.asserts.assert(entry.onFulfilled != null);
1045
1046   if (this.callbackEntriesTail_) {
1047     this.callbackEntriesTail_.next = entry;
1048     this.callbackEntriesTail_ = entry;
1049   } else {
1050     // It the work queue was empty set the head too.
1051     this.callbackEntries_ = entry;
1052     this.callbackEntriesTail_ = entry;
1053   }
1054 };

ここまでで、callbackEntries_ への callback の追加処理を確認できた。

まとめ

chromium では、Promise は以下の様な実装になっている。

  • new Promise() に登録した処理は即座に実行される。この処理が実行された時点で、 goog.Promise.State_.FULFILLEDgoog.Promise.State_.FULFILLEDthis.state_ として保存され、callback に渡される値である this.result_ も保存される。この時点で、then で callback が登録されているか否かに関わらず this.executeCallbacks_(登録された callback が無くなるまで全て実行する処理)が走る。
  • then の呼び出しで callback を登録する。この時、返り値として Promise を返すが、Promise の 初期化の際に 「callback の返り値を使う処理」を渡す。これによって chaining が実現できる。then の処理の際にも this.executeCallbacks_ が走る。
  • then に渡した callback の返り値が Promise のケースもあるので、それは Promise 生成時に走る this.resolve_ の中で this.maybeThen_ が走ることで処理される。
south37
RubyとかJavaScriptとか書きます。
http://south37.hatenablog.com/
wantedly
「シゴトでココロオドル」ためのビジネスSNS「Wantedly」の開発・運営をしています。
https://wantedlyinc.com/ja/presentations
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away