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_.FULFILLED
かgoog.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_
は、value
が Thenable
なオブジェクトの時の処理を行うっぽい。
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.executeCallbacks
を sync
で実行する。
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_.FULFILLED
かgoog.Promise.State_.FULFILLED
がthis.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_
が走ることで処理される。