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 promise p, and in the executor passed to the constructor that created p, do the time-consuming task, and resolve this new promise p 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

  1. p1 is the first promise.
  2. then() returns a new promise p2
  3. the then() remembers what p2 is, as an internal property of p1.
  4. when the fulfillment handler passed to then eventually get invoked (when p1 is resolved), the returned value is examined. If it is an object that has a property named then 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 it p1_New).
  5. Immediately, this is invoked: p1_New.then(() => { resolveP2() })
  6. Let's say p1_New is a genuine promise. When we resolve p1_New, it will resolve p2, and p2 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:

  1. 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.

1

There are 1 best solutions below

0
nonopolarity On

I understand promises more now, it seems this is the standard way.

Whatever the fulfillment handler returns: a primitive value, an object that is not a thenable, a thenable that is like a fake promise, or a real promise, will all be set up so that it will resolve p, where p is the promise that is returned by then.

This is the standard procedure, and what is called unwrapping. We can wrap and wrap a promise by many layers, when it unwraps from the inside, it will unwrap until it cannot continue.

And it is true, when an thenable fake promise, or a real promise is returned, it will trigger this action to happen underneath:

obj.then(resolveP) where resolveP is the resolve function that can resolve p.

That how that promise resolves p, and how the thenable can do asynchronous task and use the first argument passed to it as a resolve function to "resolve" p.