Promise in-depth understanding

Posted by Fixxer on Sun, 09 Jun 2019 21:48:11 +0200

Promise is simply a container containing the results of a future event (usually an asynchronous operation). Syntactically, Promise is an object from which messages for asynchronous operations can be obtained. Promise provides a unified API, and all kinds of asynchronous operations can be handled in the same way.

Promise objects have the following two characteristics.

(1) The state of the object is not affected by the outside world. Promise objects represent an asynchronous operation in three states: Pending (in progress), Resolved (completed, also known as Fulfilled) and Rejected (failed). Only the result of an asynchronous operation can determine which state it is currently, and no other operation can change that state. This is also the origin of the name Promise, which means "commitment" in English, meaning that other means can not be changed.

(2) Once the state changes, it will not change again, and this result can be obtained at any time. There are only two possibilities for the state change of Promise objects: from Pending to Resolved and from Pending to Rejected. As long as these two situations occur, the state solidifies, will not change any more, will keep this result. If the change has happened, you can add a callback function to the Promise object and get the result immediately. This is completely different from Event, which is characterized by the fact that if you miss it and listen on it, you will not get results.

With Promise objects, asynchronous operations can be expressed as synchronous operations, avoiding nested callback functions. In addition, Promise objects provide a unified interface, making it easier to control asynchronous operations.

Promise also has some drawbacks. First, Promise cannot be cancelled. Once it is newly built, it will be executed immediately and cannot be cancelled halfway. Secondly, if the callback function is not set, the errors thrown inside Promise will not be reflected outside. Thirdly, when in the Pending state, it is impossible to know which stage of progress is currently going to be (just beginning or near completion).

1.Promise's Immediate Execution

var p = new Promise(function(resolve, reject){
  console.log("create a promise");
  resolve("success");
});

console.log("after new Promise");

p.then(function(value){
  console.log(value);
});

Console output:

"create a promise"
"after new Promise"
"success"

_Promise objects represent a future event, but when creating (new) Promise, functions passed in as Promise parameters are executed immediately, except that the code executed can be asynchronous. Some students think that when the Promise object calls the then method, the function that Promise receives will execute, which is wrong. Therefore, "create a promise" in the code precedes "after new Promise" output.

2.Promise three states

var p1 = new Promise(function(resolve,reject){
  resolve(1);
});
var p2 = new Promise(function(resolve,reject){
  setTimeout(function(){
    resolve(2);  
  }, 500);      
});
var p3 = new Promise(function(resolve,reject){
  setTimeout(function(){
    reject(3);  
  }, 500);      
});

console.log(p1);
console.log(p2);
console.log(p3);
setTimeout(function(){
  console.log(p2);
}, 1000);
setTimeout(function(){
  console.log(p3);
}, 1000);

p1.then(function(value){
  console.log(value);
});
p2.then(function(value){
  console.log(value);
});
p3.catch(function(err){
  console.log(err);
});

Console output:

Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 1}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
Promise {[[PromiseStatus]]: "pending", [[PromiseValue]]: undefined}
1
2
3
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 2}
Promise {[[PromiseStatus]]: "rejected", [[PromiseValue]]: 3}

_Promise's internal implementation is a state machine. Promise has three states: pending, resolved, rejected. When Promise is created, it is in pending state; when the function parameter in Promise executes resolve, Promise changes from pending state to resolved state; if the function parameter in Promise executes not resolve method, but reject method, then Promise will change from pending state to rejected state.

When p2 and p3 were just created, both Promises output from the console were in pending state, but why was P1 in resolved state? This is because a synchronization code is executed in the function parameter of p1. As soon as Promise is created, the resolve method has been called, so the following output shows that P1 is resolved. We use two setTimeout functions to defer 1 s and output the state of p2 and p3 again. At this time, p2 and p3 have been executed, and the state becomes resolved and rejected respectively.

3. Irreversibility of Promise State

var p1 = new Promise(function(resolve, reject){
  resolve("success1");
  resolve("success2");
});

var p2 = new Promise(function(resolve, reject){
  resolve("success");
  reject("reject");
});

p1.then(function(value){
  console.log(value);
});

p2.then(function(value){
  console.log(value);
});

Console output:

"success1"
"success"

Once the Promise state becomes resolved or rejected, the state and value of Promise are fixed. No matter how you call the resolve or reject method later, you can't change its state and value. Therefore, resolution ("success2") in p1 can not change the value of p1 to success2, reject("reject") in p2 can not change the status of p2 from resolved to rejected.

4. Chain Call

var p = new Promise(function(resolve, reject){
  resolve(1);
});
p.then(function(value){               //Firstthen
  console.log(value);
  return value*2;
}).then(function(value){              //The secondthen
  console.log(value);
}).then(function(value){              //Thirdthen
  console.log(value);
  return Promise.resolve('resolve'); 
}).then(function(value){              //Fourththen
  console.log(value);
  return Promise.reject('reject');
}).then(function(value){              //Fifththen
  console.log('resolve: '+ value);
}, function(err){
  console.log('reject: ' + err);
})

Console output:

1
2
undefined
"resolve"
"reject: reject"

The then method of the_Promise object returns a new Promise object, so the then method can be invoked in a chain. The then method takes two functions as parameters. The first parameter is the callback when Promise executes successfully, and the second parameter is the callback when Promise fails. Only one of the two functions will be called, and the return value of the function will be used to create the Promise object returned by the then. The return value of these two parameters can be one of the following three cases:

(1) Return a synchronized value, or undefined (when no valid value is returned, undefined is returned by default). The then method returns a Promise object in the resolved state, and the value of the Promise object is the return value.
(2) Return another Promise, and the method will create a new Promise object to return based on the status and value of the Promise.
(3) throw a synchronous exception. The then method returns a rejected Promise with the value of the exception.
Based on the above analysis, the first one in the code returns a value of 2 (1*2) and a resolved Proise object, so the second one outputs a value of 2. There is no return value in the second one, so the default undefined is returned, and then undefined is output in the third one. The third one and the fourth one return a resolved Proise and a rejected Proise respectively, which are processed by the successful callback function in the fourth one and the failed callback function in the fifth one.

5.Promise then() callback asynchronism

var p = new Promise(function(resolve, reject){
  resolve("success");
});

p.then(function(value){
  console.log(value);
});

console.log("which one is called first ?");

Console output:

"which one is called first ?"
"success"

_Promise receives function parameters that are executed synchronously, but callback function execution in the then method is asynchronous, so "success" will be output later.

6. Exceptions in Promise

var p1 = new Promise( function(resolve,reject){
  foo.bar();
  resolve( 1 );      
});

p1.then(
  function(value){
    console.log('p1 then value: ' + value);
  },
  function(err){
    console.log('p1 then err: ' + err);
  }
).then(
  function(value){
    console.log('p1 then then value: '+value);
  },
  function(err){
    console.log('p1 then then err: ' + err);
  }
);

var p2 = new Promise(function(resolve,reject){
  resolve( 2 );    
});

p2.then(
  function(value){
    console.log('p2 then value: ' + value);
    foo.bar();
  }, 
  function(err){
    console.log('p2 then err: ' + err);
  }
).then(
  function(value){
    console.log('p2 then then value: ' + value);
  },
  function(err){
    console.log('p2 then then err: ' + err);
    return 1;
  }
).then(
  function(value){
    console.log('p2 then then then value: ' + value);
  },
  function(err){
    console.log('p2 then then then err: ' + err);
  }
);

Console output:

p1 then err: ReferenceError: foo is not defined
p2 then value: 2
p1 then then value: undefined
p2 then then err: ReferenceError: foo is not defined
p2 then then then value: 1

Exceptions in_Promise are handled by the second callback function in the then parameter (the callback that Promise failed to execute), and the exception information will be used as the value of Promise. Once the exception is handled, the subsequent Promise object returned by that will return to normal and will be handled by Promise's successful callback function. In addition, it should be noted that the callback functions of the p1 and p2 multilevel then are executed alternately, which is determined by the asynchronism of the Promise then callback.

Generally speaking, instead of defining the callback function of the Reject state in the then method (that is, the second parameter of the n), always use the catch method.

// bad
promise
  .then(function(data) {
    // success
  }, function(err) {
    // error
  });

// good
promise
  .then(function(data) { //cb
    // success
  })
  .catch(function(err) {
    // error
  });

7.Promise.resolve()

var p1 = Promise.resolve( 1 );
var p2 = Promise.resolve( p1 );
var p3 = new Promise(function(resolve, reject){
  resolve(1);
});
var p4 = new Promise(function(resolve, reject){
  resolve(p1);
});

console.log(p1 === p2); 
console.log(p1 === p3);
console.log(p1 === p4);
console.log(p3 === p4);

p4.then(function(value){
  console.log('p4=' + value);
});

p2.then(function(value){
  console.log('p2=' + value);
})

p1.then(function(value){
  console.log('p1=' + value);
})

Console output:

true
false
false
false
p2=1
p1=1
p4=1

Promise.resolve(...) ) You can take a value or a Promise object as a parameter. When the parameter is a normal value, it returns a Promise object in the resolved state, the value of which is the parameter; when the parameter is a Promise object, it returns the Promise parameter directly. Therefore, p1 === p2. But the Proise objects created by new way are all new objects, so the last three comparisons are false. In addition, why did the p4 then call first, but output the final result on the console? Because the parameter received in the resolve of p4 is a Promise object p1, resolve will unpackage P1 to get the state and value of p1, but the process is asynchronous.

The parameters of the Promise.resolve method are divided into four cases.

(1) The parameter is a Promise instance

If the parameter is a Promise instance, Promise.resolve returns the instance unchanged without any modifications.

(2) The parameter is a thenable object

The nable object refers to an object with the then method, such as the following.

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

The Promise.resolve method turns this object into a Promise object, and then immediately executes the then method of the thenable object.

let thenable = {
  then: function(resolve, reject) {
    resolve(42);
  }
};

let p1 = Promise.resolve(thenable);
p1.then(function(value) {
  console.log(value);  // 42
});

In the above code, the state of the object p1 becomes resolved after the thenable object's then method is executed, so that the callback function specified by the last then method is executed immediately, and the output is 42.

(3) A parameter is not an object with the then method, or is not an object at all.

If the parameter is an original value or an object without the then method, the Promise.resolve method returns a new Promise object in the state of Resolved.

var p = Promise.resolve('Hello');

p.then(function (s){
  console.log(s)
});
// Hello

The above code generates an instance p of a new Promise object. Since the string Hello is not an asynchronous operation (the method of judgment is that the string object does not have the then method), the status of the returned Promise instance is Resolved from generation, so the callback function is executed immediately. The parameters of the Promise.resolve method are passed to the callback function at the same time.

(4) No parameters

_Promise.resolve method allows a Promise object in Resolved state to be directly returned without parameters when invoked.

So if you want to get a Promise object, the more convenient way is to call the Promise.resolve method directly.

var p = Promise.resolve();

p.then(function () {
  // ...
});

The variable p in the above code is a Promise object.

8.resolve vs reject

var p1 = new Promise(function(resolve, reject){
  resolve(Promise.resolve('resolve'));
});

var p2 = new Promise(function(resolve, reject){
  resolve(Promise.reject('reject'));
});

var p3 = new Promise(function(resolve, reject){
  reject(Promise.resolve('resolve'));
});

p1.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);

p2.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);

p3.then(
  function fulfilled(value){
    console.log('fulfilled: ' + value);
  }, 
  function rejected(err){
    console.log('rejected: ' + err);
  }
);

Console output:

p3 rejected: [object Promise]
p1 fulfilled: resolve
p2 rejected: reject

Resolution, the first parameter in the Promise callback function, performs the "unpacking" action on Promise. That is, when the parameter of a resolve is a Promise object, the resolve "unpacks" to get the state and value of the Promise object, but the process is asynchronous. After unpacking, the status of the Promise object is resolved, so the fulfilled callback is executed; after unpacking, the status of the Promise object is rejected, so the rejected callback is executed. However, reject, the second parameter in the Promise callback function, does not have the ability to unpackage. The reject parameter is passed directly to the rejected callback in the then method. Therefore, even if p3 reject receives a Promise in resolved state, rejected is still called in the then method, and the parameter is the Promise object received by reject.

9.done()

_Promise object callback chain, whether it ends with the then method or catch method, if the last method throws an error, it may not be captured (because the error inside Promise will not bubble to the global). Therefore, we can provide a done method, always at the end of the callback chain, to ensure that any possible errors are thrown.

asyncFunc()
  .then(f1)
  .catch(r1)
  .then(f2)
  .done();

From the above code, we can see that the use of the done method can be used as the then method, providing callback functions for the Fulfilled and Rejected states, or without any parameters. However, don catches any possible errors and throws them to the whole world.

10.finally()

The final method is used to specify operations that will be performed regardless of the final state of the Promise object. The biggest difference between this method and the done method is that it accepts a common callback function as a parameter, which must be executed anyway.
Here is an example of a server using Promise to process requests and then using the final method to shut down the server.

server.listen(0)
  .then(function () {
    // run test
  })
  .finally(server.stop);

Topics: P4