I think the short version of the question is: if the fulfillment handler supplied to a .then()
returns a new promise, how does this new promise "unwrap" to the promise returned by .then()
? (unwrap seems like a terminology used in the promise technology). (and if it is not returning a new promise but just a "thenable", how does it unwrap?)
To chain several time-consuming asynchronous promises together, such as doing several network fetches described on this page or a series of animations, I think the standard method is stated as:
In the fulfillment handler passed to
.then()
, create and return a new promisep
, and in the executor passed to the constructor that createdp
, do the time-consuming task, and resolve this new promisep
when done.
Sometimes I may take this as: this is just the way it is (how to chain time-consuming promises), or it is the language feature, and you can just considered it to be happening by magic (if it is a language feature).
But is it true that this is done by standard procedure how it would be handled if the fulfillment handler returns a new promise?
I wrote out some rough draft of how it could be done below. And to state it in a few sentences, it is
p1
is the first promise.then()
returns a new promisep2
- the
then()
remembers whatp2
is, as an internal property ofp1
. - when the fulfillment handler passed to
then
eventually get invoked (whenp1
is resolved), the returned value is examined. If it is an object that has a property namedthen
and is a function, then this object is treated as a thenable, which means it can be a genuine promise or just a dummy thenable (let's call itp1_New
). - Immediately, this is invoked:
p1_New.then(() => { resolveP2() })
- Let's say
p1_New
is a genuine promise. When we resolvep1_New
, it will resolvep2
, andp2
will perform its "then" fulfillment handler, and the series of time-consuming promises can go on.
So this is the rough draft of code:
let p1 = new Promise(function(resolve, reject) {
// does something that took 10 seconds
resolve(someValue);
});
After the above code, the state of p1
is:
p1 = {
__internal__resolved_value: undefined,
__internal__state: "PENDING",
__internal__executor: function() {
// this is the executor passed to the constructor Promise().
// Something is running and the resolve(v) statement probably
// will do
// this.__internal__onResolve(v)
},
__internal__onResolve: function(resolvedValue) {
if (this.__internal__then_fulfillHandler) { // if it exists
let vFulfill = this.__internal__then_fulfillHandler(this.__internal__resolved_value);
// if the fulfillment handler returns a promise (a thenable),
// then immediately set this promise.then(fn1)
// where fn1 is to call this.__internal__resolveNewPromise()
// so as to resolve the promise newPromise that I am returning
if (vFulfill && typeof vFulfill.then === "function") { // a promise, or thenable
vFulfill.then(function() {
this.__internal__resolveNewPromise(this.__internal__resolved_value)
})
}
}
},
// in reality, the then should maintain an array of fulfillmentHandler
// because `then` can be called multiple times
then: function(fulfillmentHandler, rejectionHandler) {
this.__internal__then_fulfillHandler = fulfillmentHandler;
// create a new promise newPromise to return in any case
let newPromise = new Promise(function(resolve, reject) {
this.__internal__resolveNewPromise = resolve;
});
// if promise already resolved, then call the onResolve
// to do what is supposed to be done whenever this promise resolves
if (this.__internal__state === "RESOLVED") {
this.__internal__onResolve(this.__internal__resolved_value);
}
return newPromise;
}
}
and that's how when p1_New
is resolved, then p2
is resolved, and the fulfillment handler passed to p2.then()
will go on.
let p2 = p1.then(function() {
p1_New = new Promise(function(resolve, reject) {
// does something that took 20 seconds
resolve(someValue);
});
return p1_New;
});
p2.then(fulfillmentHandler, rejectionHandler);
But what if p1_New
is not really a promise, but just a dummy thenable
written this way:
class Thenable {
constructor(num) {
this.num = num;
}
then(resolve, reject) {
alert(resolve); // function() { native code }
// resolve with this.num*2 after the 1 second
setTimeout(() => resolve(this.num * 2), 1000); // (**)
}
}
new Promise(resolve => resolve(1))
.then(result => {
return new Thenable(result); // (*)
})
.then(alert); // shows 2 after 1000ms
it is doing the time-consuming task in its then()
. That's fine too: if we look at step 5 above:
- Immediately, this is invoked:
p1_New.then(() => { resolveP2() })
that is, then
is invoked immediately, and with the first function passed in being able to resolve p2
. So that thenable
performs some time-consuming task, and when done, call its first parameter (the function that resolves p2
), and the whole sequence can go on like before.
But in this case, it is the then()
doing the time-consuming task, not the executor doing it (the executor passed to the constructor of a new promise). In a way, it is like the then()
has become the executor.