JavaScript
Chrome
promise
Chromium
es6

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_ が走ることで処理される。