[front end] ES6 Promise object

Posted by AliceH on Sat, 05 Feb 2022 10:49:52 +0100

Promise meaning

Promise is a solution for asynchronous programming.
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 features

  1. The state of the object is not affected by the outside world. Promise has three states: 1 pending; 2. fulfilled; 3.rejected. No operation other than asynchronous operation can change this state.
  2. Once the state changes, no other changes will occur, and this result can be obtained at any time. There are only two possibilities for the state change of Promise: from pending to fully; From pending to rejected. The status at this time is also called resolved. The transformed state will always be saved, and then add a callback to the Promise object to get the result immediately.

Promise's disadvantages

  1. Promise cannot be cancelled. Once it is created, it will be executed immediately. It cannot be cancelled halfway.
  2. If the callback function is not set, the error thrown by Promise will not be reflected to the outside.
  3. When it is in pending status, it is impossible to know which stage it has reached (just started or about to be completed)

Basic Usage

Promise object is a constructor used to generate promise instances.

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

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

resolve is used to change the state of Promise object from "pending" to "resolved". It is called when the asynchronous operation is successful, and the result of the asynchronous operation is passed as a parameter; In contrast, the reject function changes the state from "pending" to "rejected"

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. The second function is optional.

Here is a simple example.

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

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

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

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;
  });
}

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);
});

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 normal values, the parameters of the resolve function may also be another Promise instance,

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

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

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.

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

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.

Promise application

Load picture
We can write the loading of the picture as a Promise. Once the loading is completed, the state of the Promise will change.

const preloadImage = function (path) {
  return new Promise(function (resolve, reject) {
    const image = new Image();
    image.onload  = resolve;
    image.onerror = reject;
    image.src = path;
  });
};

Combination of Generator function and Promise
Use the Generator function to manage the process. When an asynchronous operation is encountered, a Promise object is usually returned.

function getFoo () {
  return new Promise(function (resolve, reject){
    resolve('foo');
  });
}

const g = function* () {
  try {
    const foo = yield getFoo();
    console.log(foo);
  } catch (e) {
    console.log(e);
  }
};

function run (generator) {
  const it = generator();

  function go(result) {
    if (result.done) return result.value;

    return result.value.then(function (value) {
      return go(it.next(value));
    }, function (error) {
      return go(it.throw(error));
    });
  }

  go(it.next());
}

run(g);

In the Generator function g of the above code, there is an asynchronous operation getFoo, which returns a Promise object. The function run is used to process the Promise object and call the next method.

Promise.try()
It is often used to distinguish between asynchronous operation and asynchronous operation in the development of Promise function, but it is often not known. Because in this way, whether f contains asynchronous operations or not, the next process can be specified with the then method, and the catch method can be used to deal with the errors thrown by F. Generally, the following writing method will be adopted.

Promise.resolve().then(f)

One disadvantage of the above method is that if f is a synchronization function, it will be executed at the end of this round of event loop.

const f = () => console.log('now');
Promise.resolve().then(f);
console.log('next');
// next
// now

In the above code, function f is synchronous, but after being wrapped with Promise, it becomes asynchronous execution.

There are two ways to write synchronous functions to execute synchronously, asynchronous functions to execute asynchronously, and let them have a unified API. The answer is OK. The first method is to use async function to write.
As follows:

(async () => f())()
.then(...)
.catch(...)

The second way is to use new Promise().

const f = () => console.log('now');
(
  () => new Promise(
    resolve => resolve(f())
  )
)();
console.log('next');
// now
// next

The above code also uses the immediately executed anonymous function to execute new Promise(). In this case, the synchronization function is also executed synchronously.

Since this is a very common requirement, there is now a proposal to provide promise The try method replaces the above writing.

const f = () => console.log('now');
Promise.try(f);
console.log('next');
// now
// next

Because promise Try provides a unified processing mechanism for all operations, so if you want to use the then method to manage the process, you'd better use promise Try to pack it. This has many benefits, one of which is to better manage exceptions.

Promise.try(() => database.users.get({id: userId}))
  .then(...)
  .catch(...)

In fact, promise Try is a block of code that simulates try, like promise Catch simulates a catch code block.

Topics: Front-end ECMAScript Promise