Implement a simple Promise step by step with JavaScript, supporting asynchronous and then chain calls. Medium: Implementing a Simple Promise in Javascript – by Zhi Sun

preface

Promise comes up a lot in front-end interviews and daily development. And in many interviews these days, handwritten promises are often required.

Next, use JavaScript to implement a simple Promise step by step, supporting asynchronous and then chain calls.

Analysis of the Promise

Promise objects are used to represent the final completion (or failure) of an asynchronous operation and its resulting value, and are often used to implement asynchronous operations.

State of Promise

Promise has three states:

  • pending

    The initial state

  • fulfilled

    Status after the execution is successful

  • rejected

    Status after an execution failure

The Promise state can only be changed from pending to depressing or to Rejected. The process of changing the Promise state is known as Texas, and once the state is settled, it will not be changed again.

Parameters in the Promise constructor

The Promise constructor takes a function argument, Executor, that takes two arguments:

  • resolve
  • reject

Executing resolve changes the Promise state from Pending to fulfilled, and triggers the success callback in the then method, onFulfilled,

Executing reject changes the Promise state from Pending to Rejected and triggers the failed callback function onRejected in the then method.

Callback function parameters in the then method

The then method takes two arguments:

  • onFulfilled

    The success callback function that takes a single argument, the value passed in the resolve function

  • onRejected

    The failure callback function, which accepts an argument, the reject value passed in

If the Promise state becomes fulfilled, the success callback function onFulfilled will be executed. If the Promise state changes to Rejected, the failed callback function onRejected is executed.

Realize the Promise

Basic Promise

First, constructor takes a function executor, which in turn takes two arguments, the resolve and reject functions.

Therefore, you need to create resolve and reject functions in constructor and pass them into the executor function.

class MyPromise {
  constructor(executor) {
    const resolve = (value) = > {};

    const reject = (value) = > {};

    try {
      executor(resolve, reject);
    } catch(err) { reject(err); }}}Copy the code

Second, the Promise executes the corresponding callback function based on the state. The initial state is “Pending”. When “resolve”, the state changes from “pending” to “depressing”. When REJECT, the status changes from Pending to Rejected.

class MyPromise {
  constructor(executor) {
    this.state = 'pending';

    const resolve = (value) = > {
      if (this.state === 'pending') {
        this.state = 'fulfilled'; }};const reject = (value) = > {
      if (this.state === 'pending') {
        this.state = 'rejected'; }};try {
      executor(resolve, reject);
    } catch(err) { reject(err); }}}Copy the code

A change in the Promise state triggers the corresponding callback function in the THEN method. If the state changes from Pending to depressing, the success callback will be triggered; if the state changes from Pending to Rejected, the failure callback will be triggered.

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;

    const resolve = (value) = > {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value; }};const reject = (value) = > {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = value; }};try {
      executor(resolve, reject);
    } catch(err) { reject(err); }}then(onFulfilled, onRejected) {
    if (this.state === 'fulfilled') {
      onFulfilled(this.value);
    }

    if (this.state === 'rejected') {
      onRejected(this.value); }}}Copy the code

Next you can write some test code to test the functionality

const p1 = new MyPromise((resolve, reject) = > resolve('resolved'));
p1.then(
  (res) = > console.log(res), // resolved
  (err) = > console.log(err)
);

const p2 = new MyPromise((resolve, reject) = > reject('rejected'));
p2.then(
  (res) = > console.log(res),
  (err) = > console.log(err) // rejected
);
Copy the code

However, if you test with the following code, you will find nothing.

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

p1.then(
  (res) = > console.log(res),
  (err) = > console.log(err)
);

const p2 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > reject('rejected'), 1000);
});

p2.then(
  (res) = > console.log(res),
  (err) = > console.log(err)
);
Copy the code

This is because the Promise is still pending when the THEN method is called. The onFulfilled and onRejected callback functions are not executed.

So the next step is to support asynchrony.

Support for asynchronous promises

In order to support asynchronism, the onFulfilled and onRejected callback functions need to be saved first. Once the Promise state changes, the corresponding callback functions will be implemented immediately.

⚠ : There is one detail to note here, namelyonFulfilledCallbacksandonRejectedCallbacksIs an array, because promises can be called multiple times, so there will be multiple callback functions.

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) = > {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;

        this.onFulfilledCallbacks.forEach((fn) = >fn(value)); }};const reject = (value) = > {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = value;

        this.onRejectedCallbacks.forEach((fn) = >fn(value)); }};try {
      executor(resolve, reject);
    } catch(err) { reject(err); }}then(onFulfilled, onRejected) {
    if (this.state === 'pending') {
      this.onFulfilledCallbacks.push(onFulfilled);
      this.onRejectedCallbacks.push(onRejected);
    }

    if (this.state === 'fulfilled') {
      onFulfilled(this.value);
    }

    if (this.state === 'rejected') {
      onRejected(this.value); }}}Copy the code

You can then test the functionality with the previous test code

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

p1.then(
  (res) = > console.log(res), // resolved
  (err) = > console.log(err)
);

const p2 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > reject('rejected'), 1000);
});

p2.then(
  (res) = > console.log(res),
  (err) = > console.log(err) // rejected
);
Copy the code

However, if you test with the following code, you will find that an error is reported.

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

p1.then(
  (res) = > console.log(res),
  (err) = > console.log(err)
).then(
  (res) = > console.log(res),
  (err) = > console.log(err)
); // Uncaught TypeError: Cannot read property 'then' of undefined
Copy the code

This is because the first THEN method does not return any value, yet the then method is called consecutively.

Therefore, the next step is to implement the THEN chain call.

supportthenThe Promise of the chain call

To support then chain calls, the THEN method needs to return a new Promise.

Therefore, the then method needs to be modified to return a new Promise. After the onFulfilled or onRejected callback function of the Promise is completed, the resolve or Reject function of the new Promise will be implemented.

class MyPromise {
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) = > {
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() = > {
          try {
            const fulfilledFromLastPromise = onFulfilled(this.value);
            resolve(fulfilledFromLastPromise);
          } catch(err) { reject(err); }});this.onRejectedCallbacks.push(() = > {
          try {
            const rejectedFromLastPromise = onRejected(this.value);
            reject(rejectedFromLastPromise);
          } catch(err) { reject(err); }}); }if (this.state === 'fulfilled') {
        try {
          const fulfilledFromLastPromise = onFulfilled(this.value);
          resolve(fulfilledFromLastPromise);
        } catch(err) { reject(err); }}if (this.state === 'rejected') {
        try {
          const rejectedFromLastPromise = onRejected(this.value);
          reject(rejectedFromLastPromise);
        } catch(err) { reject(err); }}}); }}Copy the code

You can then test the functionality with the following code

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

p1.then(
  (res) = > {
    console.log(res); // resolved
    return res;
  },
  (err) = > console.log(err)
).then(
  (res) = > console.log(res), // resolved
  (err) = > console.log(err)
);

const p2 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > reject('rejected'), 1000);
});

p2.then(
  (res) = > console.log(res),
  (err) = > {
    console.log(err); // rejected
    throw new Error('rejected');
  }
).then(
  (res) = > console.log(res),
  (err) = > console.log(err) // Error: rejected
);
Copy the code

However, if you test this code instead, you will see that the successful callback function in the second THEN method does not output as expected (‘ resolved ‘), but instead outputs the Promise returned in the onFulfilled callback function of the previous THEN method.

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

p1.then(
  (res) = > {
    console.log(res); // resolved
    return new MyPromise((resolve, reject) = > {
      setTimeout(() = > resolve('resolved'), 1000); })},(err) = > console.log(err)
).then(
  (res) = > console.log(res), // MyPromise {state: "pending"}
  (err) = > console.log(err)
);
Copy the code

This is because after the onFulfilled/onRejected callback is completed, we will simply pass the value returned by onFulfilled/onRejected into the resolve/ Reject function and execute it. This is a big pity/onFulfilled, which will return a new Promise after the completion of onFulfilled/onRejected. Therefore, the success callback function of the second THEN method will output the Promise returned from the success callback function of the last THEN method. Therefore, the next step is to solve this problem.

First of all, you can change the above test code to another way of writing, which is convenient for sorting out ideas.

const p1 = new MyPromise((resolve, reject) = > {
  setTimeout(() = > resolve('resolved'), 1000);
});

const p2 = p1.then(
  (res) = > {
    console.log(res);
    const p3 = new MyPromise((resolve, reject) = > {
      setTimeout(() = > resolve('resolved'), 1000);
    });

    return p3;
  },
  (err) = > console.log(err)
);

p2.then(
  (res) = > console.log(res),
  (err) = > console.log(err)
);
Copy the code

As you can see, there are three promises:

  • The first Promise

    That is, p1 constructed by new

  • The second Promise

    That is p2 returned by calling the then method

  • The third Promise

    That is p3 returned in the success callback parameter of the p1.then method

The problem is that P3 is still pending when P2’s then method is called.

In order for the p2.then callback function to correctly output resolve/reject in P3, wait for p3 state to change and pass the changed value into P2. In other words, the order of the three Promise states should be P1 –> P3 –> P2.

class MyPromise {
  then(onFulfilled, onRejected) {
    return new MyPromise((resolve, reject) = > {
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() = > {
          try {
            const fulfilledFromLastPromise = onFulfilled(this.value);

            if (fulfilledFromLastPromise instanceof MyPromise) {
              fulfilledFromLastPromise.then(resolve, reject);
            } else{ resolve(fulfilledFromLastPromise); }}catch(err) { reject(err); }});this.onRejectedCallbacks.push(() = > {
          try {
            const rejectedFromLastPromise = onRejected(this.value);

            if (rejectedFromLastPromise instanceof MyPromise) {
              rejectedFromLastPromise.then(resolve, reject);
            } else{ reject(rejectedFromLastPromise); }}catch(err) { reject(err); }}); }if (this.state === 'fulfilled') {
        try {
          const fulfilledFromLastPromise = onFulfilled(this.value);

          if (fulfilledFromLastPromise instanceof MyPromise) {
            fulfilledFromLastPromise.then(resolve, reject);
          } else{ resolve(fulfilledFromLastPromise); }}catch(err) { reject(err); }}if (this.state === 'rejected') {
        try {
          const rejectedFromLastPromise = onRejected(this.value);

          if (rejectedFromLastPromise instanceof MyPromise) {
            rejectedFromLastPromise.then(resolve, reject);
          } else{ reject(rejectedFromLastPromise); }}catch(err) { reject(err); }}}); }}Copy the code

The final version Promise

Finally, a simple Promise is completed, supporting asynchronous and then chain calls. The complete code is as follows:

class MyPromise {
  constructor(executor) {
    this.state = 'pending';
    this.value = null;
    this.onFulfilledCallbacks = [];
    this.onRejectedCallbacks = [];

    const resolve = (value) = > {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        this.onFulfilledCallbacks.forEach((fn) = >fn(value)); }};const reject = (value) = > {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.value = value;
        this.onRejectedCallbacks.forEach((fn) = >fn(value)); }};try {
      executor(resolve, reject);
    } catch(err) { reject(err); }}then(onFulfilled, onRejected) {
    return new Promise((resolve, reject) = > {
      if (this.state === 'pending') {
        this.onFulfilledCallbacks.push(() = > {
          try {
            const fulfilledFromLastPromise = onFulfilled(this.value);
            if (fulfilledFromLastPromise instanceof Promise) {
              fulfilledFromLastPromise.then(resolve, reject);
            } else{ resolve(fulfilledFromLastPromise); }}catch(err) { reject(err); }});this.onRejectedCallbacks.push(() = > {
          try {
            const rejectedFromLastPromise = onRejected(this.value);
            if (rejectedFromLastPromise instanceof Promise) {
              rejectedFromLastPromise.then(resolve, reject);
            } else{ reject(rejectedFromLastPromise); }}catch(err) { reject(err); }}); }if (this.state === 'fulfilled') {
        try {
          const fulfilledFromLastPromise = onFulfilled(this.value);
          if (fulfilledFromLastPromise instanceof Promise) {
            fulfilledFromLastPromise.then(resolve, reject);
          } else{ resolve(fulfilledFromLastPromise); }}catch(err) { reject(err); }}if (this.state === 'rejected') {
        try {
          const rejectedFromLastPromise = onRejected(this.value);
          if (rejectedFromLastPromise instanceof Promise) {
            rejectedFromLastPromise.then(resolve, reject);
          } else{ reject(rejectedFromLastPromise); }}catch(err) { reject(err); }}}); }}Copy the code