Promise callbacks returning promises

2019-01-21 06:25发布

问题:

With regard to these great two sources: NZakas - Returning Promises in Promise Chains and MDN Promises, I would like to ask the following:

Each time that we return a value from a promise fulfillment handler, how is that value passed to the new promise returned from that same handler?

For instance,

let p1 = new Promise(function(resolve, reject) {
    resolve(42);
});

let p2 = new Promise(function(resolve, reject) {
    resolve(43);
});

let p3 = p1.then(function(value) {
    // first fulfillment handler
    console.log(value);     // 42
    return p2;
});

p3.then(function(value) {
    // second fulfillment handler
    console.log(value);     // 43
});

In this example, p2 is a promise. p3 is also a promise originating from p1's fulfillment handler. However p2 !== p3. Instead p2 somehow magically resolves to 43 (how?) and that value is then passed to p3's fulfillment handler. Even the sentence here is confusing.

Could you please explain to me what exactly is going on here? I am totally confused over this concept.

回答1:

Let’s say that throwing inside then() callback rejects the result promise with a failure, and returning from then() callback fulfills the result promise with a success value.

let p2 = p1.then(() => {
  throw new Error('lol')
})
// p2 was rejected with Error('lol')

let p3 = p1.then(() => {
  return 42
})
// p3 was fulfilled with 42

But sometimes, even inside the continuation, we don’t know whether we have succeeded or not. We need more time.

return checkCache().then(cachedValue => {
  if (cachedValue) {
    return cachedValue
  }

  // I want to do some async work here
})

However, if I do async work there, it would be too late to return or throw, wouldn’t it?

return checkCache().then(cachedValue => {
  if (cachedValue) {
    return cachedValue
  }

  fetchData().then(fetchedValue => {
    // Doesn’t make sense: it’s too late to return from outer function by now.
    // What do we do?

    // return fetchedValue
  })
})

This is why Promises wouldn’t be useful if you couldn’t resolve to another Promise.

It doesn’t mean that in your example p2 would become p3. They are separate Promise objects. However, by returning p2 from then() that produces p3 you are saying “I want p3 to resolve to whatever p2 resolves, whether it succeeds or fails”.

As for how this happens, it’s implementation-specific. Internally you can think of then() as creating a new Promise. The implementation will be able to fulfill or reject it whenever it likes. Normally, it will automatically fulfill or reject it when you return:

// Warning: this is just an illustration
// and not a real implementation code.
// For example, it completely ignores
// the second then() argument for clarity,
// and completely ignores the Promises/A+
// requirement that continuations are
// run asynchronously.

then(callback) {
  // Save these so we can manipulate
  // the returned Promise when we are ready
  let resolve, reject

  // Imagine this._onFulfilled is an internal
  // queue of code to run after current Promise resolves.
  this._onFulfilled.push(() => {
    let result, error, succeeded
    try {
      // Call your callback!
      result = callback(this._result)
      succeeded = true
    } catch (err) {
      error = err
      succeeded = false
    }

    if (succeeded) {
      // If your callback returned a value,
      // fulfill the returned Promise to it
      resolve(result)
    } else {
      // If your callback threw an error,
      // reject the returned Promise with it
      reject(error)
    }
  })

  // then() returns a Promise
  return new Promise((_resolve, _reject) => {
    resolve = _resolve
    reject = _reject
  })
}

Again, this is very much pseudo-code but shows the idea behind how then() might be implemented in Promise implementations.

If we want to add support for resolving to a Promise, we just need to modify the code to have a special branch if the callback you pass to then() returned a Promise:

    if (succeeded) {
      // If your callback returned a value,
      // resolve the returned Promise to it...
      if (typeof result.then === 'function') {
        // ...unless it is a Promise itself,
        // in which case we just pass our internal
        // resolve and reject to then() of that Promise
        result.then(resolve, reject)
      } else {
        resolve(result)
      }
    } else {
      // If your callback threw an error,
      // reject the returned Promise with it
      reject(error)
    }
  })

Let me clarify again that this is not an actual Promise implementation and has big holes and incompatibilities. However it should give you an intuitive idea of how Promise libraries implement resolving to a Promise. After you are comfortable with the idea, I would recommend you to take a look at how actual Promise implementations handle this.



回答2:

Basically p3 is return-ing an another promise : p2. Which means the result of p2 will be passed as a parameter to the next then callback, in this case it resolves to 43.

Whenever you are using the keyword return you are passing the result as a parameter to next then's callback.

let p3 = p1.then(function(value) {
    // first fulfillment handler
    console.log(value);     // 42
    return p2;
});

Your code :

p3.then(function(value) {
    // second fulfillment handler
    console.log(value);     // 43
});

Is equal to:

p1.then(function(resultOfP1) {
    // resultOfP1 === 42
    return p2; // // Returning a promise ( that might resolve to 43 or fail )
})
.then(function(resultOfP2) {
    console.log(resultOfP2) // '43'
});

Btw, I've noticed that you are using ES6 syntax, you can have a lighter syntax by using fat arrow syntax :

p1.then(resultOfP1 => p2) // the `return` is implied since it's a one-liner
.then(resultOfP2 => console.log(resultOfP2)); 


回答3:

In this example, p2 is a promise. p3 is also a promise originating from p1's fulfillment handler. However p2 !== p3. Instead p2 somehow magically resolves to 43 (how?) and that value is then passed to p3's fulfillment handler. Even the sentence here is confusing.

a simplified version how this works (only pseudocode)

function resolve(value){
    if(isPromise(value)){
        value.then(resolve, reject);
    }else{
        //dispatch the value to the listener
    }
}

the whole thing is quite more complicated since you have to take care, wether the promise has already been resolved and a few more things.



回答4:

I'll try to answer the question "why then callbacks can return Promises themselves" more canonical. To take a different angle, I compare Promises with a less complex and confusing container type - Arrays.

A Promise is a container for a future value. An Array is a container for an arbitrary number of values.

We can't apply normal functions to container types:

const sqr = x => x * x;
const xs = [1,2,3];
const p = Promise.resolve(3);

sqr(xs); // fails
sqr(p); // fails

We need a mechanism to lift them into the context of a specific container:

xs.map(sqr); // [1,4,9]
p.then(sqr); // Promise {[[PromiseValue]]: 9}

But what happens when the provided function itself returns a container of the same type?

const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];
const p = Promise.resolve(3);

xs.map(sqra); // [[1],[4],[9]]
p.then(sqrp); // Promise {[[PromiseValue]]: 9}

sqra acts like expected. It just returns a nested container with the correct values. This is obviously not very useful though.

But how can the result of sqrp be interpreted? If we follow our own logic, it had to be something like Promise {[[PromiseValue]]: Promise {[[PromiseValue]]: 9}} - but it is not. So what magic is going on here?

To reconstruct the mechanism we merely need to adapt our map method a bit:

const flatten = f => x => f(x)[0];
const sqra = x => [x * x];
const sqrp = x => Promise.resolve(x * x);
const xs = [1,2,3];

xs.map(flatten(sqra))

flatten just takes a function and a value, applies the function to the value and unwraps the result, thus it reduces a nested array structure by one level.

Simply put, then in the context of Promises is equivalent to map combined with flatten in the context of Arrays. This behavior is extremely important. We can apply not only normal functions to a Promise but also functions that itself return a Promise.

In fact this is the domain of functional programming. A Promise is a specific implementation of a monad, then is bind/chain and a function that returns a Promise is a monadic function. When you understand the Promise API you basically understand all monads.