Ruan Yifeng Promise learning

Posted by Dvorak on Sun, 06 Mar 2022 15:17:28 +0100

Promise meaning

Promise is a solution for asynchronous programming, which is more reasonable and powerful than traditional solutions - callback functions and events. It was first proposed and implemented by the community. ES6 has written it into the language standard, unified the usage, and provided promise objects natively.

Promise is simply a container that holds the results of an event (usually an asynchronous operation) that will not end in the future. Syntactically speaking, promise is an object from which you can get the message of asynchronous operation. Promise provides a unified API, and various asynchronous operations can be processed in the same way.

Promise objects have the following two characteristics.

  1. The state of the object is not affected by the outside world. The Promise object represents an asynchronous operation with three states: pending, completed, and rejected. Only the result of asynchronous operation can determine the current state, and no other operation can change this state. This is also the origin of Promise. Its English meaning is "commitment", which means that other means cannot be changed.

  2. Once the state changes, it will not change again. This result can be obtained at any time. There are only two possibilities for the state of Promise object to change: from pending to fully and from pending to rejected. As long as these two situations occur, the state will solidify, will not change again, and will maintain this result all the time. At this time, it is called
    resolved. If the change has occurred, you can add a callback function to the Promise object and get the result immediately. This is completely different from the Event. The characteristic of the Event is that if you miss it and listen again, you won't get the result.

Note that for the convenience of writing, the resolved unified in the later part of this chapter only refers to the fully qualified state, not including the rejected state.

With Promise object, asynchronous operation can be expressed in the process of synchronous operation, avoiding layers of nested callback functions. In addition, Promise objects provide a unified interface, making it easier to control asynchronous operations.

Promise also has some disadvantages.

First of all, Promise cannot be cancelled. Once it is created, it will be executed immediately. It cannot be cancelled halfway. Secondly, if the callback function is not set, the errors thrown by Promise will not be reflected to the outside. Third, when it is in the pending state, it is impossible to know which stage it has reached (just started or about to be completed).

If some events happen repeatedly, generally speaking, using Stream mode is a better choice than deploying Promise.

Basic Usage

ES6 stipulates that Promise object is a constructor used to generate Promise instance.

The following code creates a Promise instance.

const promise = new Promise(function(resolve, reject) {
  // ... some code

  if (/* Asynchronous operation succeeded */){
    resolve(value);
  } else {
    reject(error);
  }
});

The Promise constructor takes a function as an argument, and the two parameters of the function are resolve and reject. They are two functions provided by the JavaScript engine and do not need to be deployed by themselves.

The resolve function is used to change the state of Promise object from "incomplete" to "successful" (i.e. from pending to resolved), call it when the asynchronous operation is successful, and pass the result of the asynchronous operation as a parameter; The reject function is used to change the state of the Promise object from incomplete to failed (that is, from pending to rejected), call it when the asynchronous operation fails, and pass the error reported by the asynchronous operation as a parameter.

After the Promise instance is generated, you can use the then method to specify the callback functions of resolved state and rejected state respectively.

promise.then(function(value) {
  // success
}, function(error) {
  // failure
});

The then method can accept two callback functions as parameters. The first callback function is called when the state of the Promise object changes to resolved, and the second callback function is called when the state of the Promise object changes to rejected. Both functions are optional and do not have to be provided. They all accept the value from the Promise object as a parameter.

The following is a simple example of a Promise object.

function timeout(ms) {
  return new Promise((resolve, reject) => {
    setTimeout(resolve, ms, 'done');
  });
}

timeout(100).then((value) => {
  console.log(value);
});

In the above code, the timeout method returns a Promise instance, which represents the result that will not occur for a period of time. After the specified time (ms parameter), the state of Promise instance changes to resolved, and the callback function bound by then method will be triggered.

Promise will be executed immediately after it is created.

let promise = new Promise(function(resolve, reject) {
  console.log('Promise');
  resolve();
});

promise.then(function() {
  console.log('resolved.');
});

console.log('Hi!');

// Promise
// Hi!
// resolved

In the above code, Promise is executed immediately after it is created, so Promise is output first. Then, the callback function specified by the then method will not be executed until all synchronization tasks in the current script have been executed, so the resolved is finally output.

The following is an example of loading pictures asynchronously.

function loadImageAsync(url) {
  return new Promise(function(resolve, reject) {
    const image = new Image();

    image.onload = function() {
      resolve(image);
    };

    image.onerror = function() {
      reject(new Error('Could not load image at ' + url));
    };

    image.src = url;
  });
}

In the above code, Promise is used to wrap an asynchronous operation of image loading. If the load is successful, the resolve method is called; otherwise, the reject method is called.

The following is an example of Ajax operation implemented with Promise object.

const getJSON = function(url) {
  const promise = new Promise(function(resolve, reject){
    const handler = function() {
      if (this.readyState !== 4) {
        return;
      }
      if (this.status === 200) {
        resolve(this.response);
      } else {
        reject(new Error(this.statusText));
      }
    };
    const client = new XMLHttpRequest();
    client.open("GET", url);
    client.onreadystatechange = handler;
    client.responseType = "json";
    client.setRequestHeader("Accept", "application/json");
    client.send();

  });

  return promise;
};

getJSON("/posts.json").then(function(json) {
  console.log('Contents: ' + json);
}, function(error) {
  console.error('Error ', error);
});

In the above code, getJSON is the encapsulation of XMLHttpRequest object, which is used to issue an HTTP request for JSON data and return a Promise object. It should be noted that inside getJSON, the resolve function and reject function are called with parameters.

If the resolve function and reject function are called with parameters, their parameters are passed to the callback function. The parameter of reject function is usually the instance of Error object, which indicates the Error thrown; In addition to the normal value, the parameter of the resolve function may also be another Promise instance, such as the following.

const p1 = new Promise(function (resolve, reject) {
  // ...
});

const p2 = new Promise(function (resolve, reject) {
  // ...
  resolve(p1);
})

In the above code, p1 and p2 are both instances of Promise, but the resolve method of p2 takes p1 as a parameter, that is, the result of an asynchronous operation is to return another asynchronous operation.

Note that at this time, the state of p1 will be transferred to p2, that is, the state of p1 determines the state of p2. If the state of p1 is pending, the callback function of p2 will wait for the state of p1 to change; If the status of p1 is resolved or rejected, the callback function of p2 will be executed immediately.

const p1 = new Promise(function (resolve, reject) {
  setTimeout(() => reject(new Error('fail')), 3000)
})

const p2 = new Promise(function (resolve, reject) {
  setTimeout(() => resolve(p1), 1000)
})

p2
  .then(result => console.log(result))
  .catch(error => console.log(error))
// Error: fail

In the above code, p1 is a Promise and becomes rejected after 3 seconds. The state of p2 changes after 1 second, and the resolve method returns p1. Since p2 returns another Promise, p2's own state is invalid. The state of p2 is determined by the state of p1. Therefore, the following then statements become for the latter (p1). After another 2 seconds, p1 becomes rejected, causing the callback function specified by the catch method to be triggered.

Note that calling resolve or reject does not terminate the execution of Promise's parameter function.

new Promise((resolve, reject) => {
  resolve(1);
  console.log(2);
}).then(r => {
  console.log(r);
});
// 2
// 1

In the above code, after calling resolve(1), the following console.log(2) will still be executed and printed first. This is because the immediately resolved Promise is executed at the end of this round of event loop, which is always later than the synchronization task of this round of loop.

Generally speaking, after calling resolve or reject, Promise's mission is completed. Subsequent operations should be placed in the then method, not directly after resolve or reject. Therefore, it is best to add a return statement in front of them so that there will be no accidents.

new Promise((resolve, reject) => {
  return resolve(1);
  // Subsequent statements will not be executed
  console.log(2);
})

Promise.prototype.then()

Promise instance has a then method, that is, the then method is defined in the prototype object promise On prototype. Its function is to add a callback function when the state changes for promise instance. The first parameter of the callback function is the state of the second callback function.

The then method returns a new Promise instance (note that it is not the original Promise instance). Therefore, you can use the chain writing method, that is, after the then method, you can call another then method.

getJSON("/posts.json").then(function(json) {
  return json.post;
}).then(function(post) {
  // ...
});

The above code uses the then method to specify two callback functions in turn. After the first callback function is completed, the returned result will be passed into the second callback function as a parameter.

Using chained then, you can specify a set of callback functions to be called in order. At this time, the previous callback function may return a Promise object (i.e. asynchronous operation). At this time, the latter callback function will wait for the state of the Promise object to change before being called.

getJSON("/post/1.json").then(function(post) {
  return getJSON(post.commentURL);
}).then(function (comments) {
  console.log("resolved: ", comments);
}, function (err){
  console.log("rejected: ", err);
});

In the above code, the callback function specified by the first then method returns another Promise object. At this time, the callback function specified by the second then method will wait for the state of the new Promise object to change. If the status changes to resolved, the first callback function will be called. If the status changes to rejected, the second callback function will be called.

If the arrow function is used, the above code can be written more succinctly.

getJSON("/post/1.json").then(
  post => getJSON(post.commentURL)
).then(
  comments => console.log("resolved: ", comments),
  err => console.log("rejected: ", err)
);

Promise.prototype.catch()

Promise. prototype. The catch () method is then(null, rejection) or An alias for then(undefined, rejection), which specifies the callback function when an error occurs.

getJSON('/posts.json').then(function(posts) {
  // ...
}).catch(function(error) {
  // Error occurred when processing getJSON and the previous callback function
  console.log('An error has occurred!', error);
});

In the above code, the getJSON() method returns a Promise object. If the state of the object changes to resolved, the callback function specified by the then() method will be called; If the asynchronous operation throws an error, the status will change to rejected, and the callback function specified by the catch() method will be called to handle the error. In addition, if the callback function specified by the then() method throws an error during operation, it will also be caught by the catch() method.

p.then((val) => console.log('fulfilled:', val))
  .catch((err) => console.log('rejected', err));

// Equivalent to
p.then((val) => console.log('fulfilled:', val))
  .then(null, (err) => console.log("rejected:", err));
Here is an example.

const promise = new Promise(function(resolve, reject) {
  throw new Error('test');
});
promise.catch(function(error) {
  console.log(error);
});
// Error: test

In the above code, promise throws an error and is caught by the callback function specified by the catch() method. Note that the above writing is equivalent to the following two writing methods.

// Writing method I
const promise = new Promise(function(resolve, reject) {
  try {
    throw new Error('test');
  } catch(e) {
    reject(e);
  }
});
promise.catch(function(error) {
  console.log(error);
});

// Writing method 2
const promise = new Promise(function(resolve, reject) {
  reject(new Error('test'));
});
promise.catch(function(error) {
  console.log(error);
});

Comparing the above two methods, we can find that the reject() method is equivalent to throwing an error.

If the Promise state has changed to resolved, it is invalid to throw an error again.

const promise = new Promise(function(resolve, reject) {
  resolve('ok');
  throw new Error('test');
});
promise
  .then(function(value) { console.log(value) })
  .catch(function(error) { console.log(error) });
// ok

In the above code, Promise throws an error after the resolve statement and will not be caught, which is equal to not throwing. Because once the Promise state changes, it will remain in this state forever and will not change again.

The error of Promise object is "bubbling" and will be passed back until it is caught. That is, errors are always caught by the next catch statement.

getJSON('/post/1.json').then(function(post) {
  return getJSON(post.commentURL);
}).then(function(comments) {
  // some code
}).catch(function(error) {
  // Handle the errors generated by the first three promises
});

In the above code, there are three Promise objects: one generated by getJSON() and two generated by then(). The error thrown by any one of them will be caught by the last catch().

Generally speaking, don't define the callback function of Reject status (that is, the second parameter of then) in the then() method. 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
  });

In the above code, the second writing method is better than the first writing method because the second writing method can catch the errors in the execution of the previous then method and is closer to the synchronous writing method (try/catch). Therefore, it is recommended to always use the catch() method instead of the second parameter of the then() method.

Different from the traditional try/catch code block, if the catch() method is not used to specify the callback function for error handling, the error thrown by the Promise object will not be passed to the outer code, that is, there will be no response.

Promise.prototype.finally()

The finally() method is used to specify the operation that will be performed regardless of the final state of the Promise object. This method is the standard introduced by ES2018.

promise
.then(result => {···})
.catch(error => {···})
.finally(() => {···});

In the above code, regardless of the final state of promise, the callback function specified by the finally method will be executed after the callback function specified by then or catch is executed.

The following is an example. The server uses Promise to process the request, and then uses the finally method to turn off the server.

server.listen(port)
  .then(function () {
    // ...
  })
  .finally(server.stop);

The callback function of the finally method does not accept any parameters, which means that there is no way to know whether the previous Promise state is fully or rejected. This shows that the operations in the finally method should be state independent and independent of the Promise execution result.

finally is essentially a special case of the then method.

promise
.finally(() => {
  // sentence
});

// Equivalent to
promise
.then(
  result => {
    // sentence
    return result;
  },
  error => {
    // sentence
    throw error;
  }
);

In the above code, if the finally method is not used, the same statement needs to be written once for success and once for failure. With the finally method, you only need to write it once.

Its implementation is also very simple.

Promise.prototype.finally = function (callback) {
  let P = this.constructor;
  return this.then(
    value  => P.resolve(callback()).then(() => value),
    reason => P.resolve(callback()).then(() => { throw reason })
  );
};

In the above code, no matter whether the previous Promise is fully or rejected, the callback function callback will be executed.

From the above implementation, we can also see that the finally method always returns the original value.

// The value of resolve is undefined
Promise.resolve(2).then(() => {}, () => {})

// The value of resolve is 2
Promise.resolve(2).finally(() => {})

// The value of reject is undefined
Promise.reject(3).then(() => {}, () => {})

// The value of reject is 3
Promise.reject(3).finally(() => {})

Promise.all()

Promise. The all () method is used to wrap multiple promise instances into a new promise instance.

const p = Promise.all([p1, p2, p3]);

In the above code, Promise The all () method accepts an array as a parameter. p1, p2 and p3 are Promise instances. If they are not, the Promise described below will be called first Resolve method, convert the parameter to Promise instance, and then conduct further processing. In addition, Promise The parameter of the all () method can not be an array, but it must have an Iterator interface, and each member returned is a Promise instance.

The state of p is determined by p1, p2 and p3, which can be divided into two cases.

(1) Only when the states of p1, p2 and p3 become fully, the state of p will become fully. At this time, the return values of p1, p2 and p3 form an array and are passed to the callback function of p.

(2) As long as one of p1, p2 and p3 is rejected, the state of p becomes rejected. At this time, the return value of the first rejected instance will be passed to the callback function of p.

Here is a specific example.

// Generate an array of Promise objects
const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(function (posts) {
  // ...
}).catch(function(reason){
  // ...
});

In the above code, promises is an array containing six promise instances. Promise will be called only when the state of these six instances becomes fully or one of them becomes rejected Callback function after all method.

Here is another example.

const databasePromise = connectDatabase();

const booksPromise = databasePromise
  .then(findAllBooks);

const userPromise = databasePromise
  .then(getCurrentUser);

Promise.all([
  booksPromise,
  userPromise
])
.then(([books, user]) => pickTopRecommendations(books, user));

In the above code, booksPromise and userPromise are two asynchronous operations. The callback function pickTopRecommendations will not be triggered until their results are returned.

Promise.race()

Promise. The race () method also wraps multiple promise instances into a new promise instance.

const p = Promise.race([p1, p2, p3]);
In the above code, as long as one instance of p1, p2 and p3 changes the state first, the state of p will change accordingly. The return value of the Promise instance that changed first is passed to the callback function of p.

Promise. The parameters of race() method are the same as promise Like the all () method, if it is not a promise instance, it will first call promise Resolve () method, convert the parameter to promise instance, and then further processing.

The following is an example. If no result is obtained within the specified time, change the Promise status to reject, otherwise change to resolve.

const p = Promise.race([
  fetch('/resource-that-may-take-a-while'),
  new Promise(function (resolve, reject) {
    setTimeout(() => reject(new Error('request timeout')), 5000)
  })
]);

p
.then(console.log)
.catch(console.error);

In the above code, if the fetch method cannot return the result within 5 seconds, the state of variable p will change to rejected, thus triggering the callback function specified by the catch method.

Topics: Javascript Front-end