10
头图
Use JavaScript to implement a simple Promise step by step, supporting asynchronous and then chain calls.
Translated and organized from Medium: Implementing a simple Promise in Javascript-by Zhi Sun

Preface

In front-end interviews and daily development, Promise is often exposed. And in many interviews nowadays, you are often asked to write Promises by hand.

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

Analyze Promise

The Promise object is used to represent the final completion (or failure) of an asynchronous operation and its result value. It is commonly used to implement asynchronous operations.

Promise status

Promise has three states:

  • pending

    Initial state

  • fulfilled

    Status after successful execution

  • rejected

    Status after execution failure

The Promise state can only be changed pending fulfilled or from pending to rejected . The process of Promise state change is called settled , and once the state changes, it will not be changed again in the future.

Parameters in the Promise constructor

The Promise constructor receives a function parameter executor , and the function receives two parameters:

  • resolve
  • reject

Executing resolve will change the Promise status from pending to fulfilled , and trigger the success callback function onFulfilled then method,

Executing reject will change the Promise status from pending to rejected , and trigger the failure callback function onRejected then method.

Callback function parameters in the then method

then method receives two parameters:

  • onFulfilled

    Success callback function, receiving a parameter, that is, the value passed in the resolve

  • onRejected

    Failure callback function, receiving a parameter, that is, the value passed in the reject

If the Promise status changes to fulfilled , the success callback function onFulfilled will be executed; if the Promise status changes to rejected , the failure callback function onRejected will be executed.

Realize Promise

Basic Promise

First, constructor receives a function executor , and the function receives two parameters, which are functions resolve and reject

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

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

    const reject = (value) => {};

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

Secondly, Promise will execute the corresponding callback function according to the state. The initial state is pending , when resolve , the state changes pending fulfilled ; when reject , the state changes pending 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);
    }
  }
}

After the Promise state changes, the corresponding callback function in the then If the status changes pending fulfilled , the success callback function will be triggered. If the status changes pending rejected , the failure callback function 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);
    }
  }
}

Next, you can write some test code to test the function

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

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

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

This is because when the then method is called, the Promise is still in the pending state. onFulfilled and onRejected callback functions are not executed.

Therefore, the next step is to support asynchrony.

Support asynchronous Promise

In order to support asynchronous, you need to save the onFulfilled and onRejected callback functions. Once the Promise status changes, the corresponding callback function will be executed immediately.

: There is a detail that needs attention, that is, onFulfilledCallbacks and onRejectedCallbacks are arrays, because Promise may 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);
    }
  }
}

Next, you can use the previous test code to test the function

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

But if you use the following code to test, you will find an error.

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

This is because the first then method did not return any value, but the then method was continuously called.

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

Promise that supports then

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

Therefore, you need to modify the then method to return a new Promise. onRejected onFulfilled or 061ad7161c1a89 is executed, execute the resolve or reject function of the new Promise.

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

Next, you can test the function 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
);

However, if you use the following code to test, you will find then method does not output as expected ('resolved'), but outputs the Promise returned in the onFulfilled then

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

This is because onFulfilled / onRejected after executing the callback function, simply will onFulfilled / onRejected perform complete return values passed resolve / reject function to perform, and did not consider onFulfilled / onRejected executing the return of a new case of Promise , So the second success callback function of the then method directly outputs the Promise returned in the success callback function of the then Therefore, this problem needs to be solved next.

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

As you can see, there are three Promises:

  • The first Promise

    That is, p1 constructed new

  • The second Promise

    That is, p2 returned by calling the then

  • The third Promise

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

The problem now is that then method is called, p3 is still in the pending state.

In order to realize that the callback function in the p2.then resolve / reject in p3, you need to wait for the state of p3 to change and then transfer the changed value to resolve / reject in p2. In other words, the sequence of the state changes of the three Promises 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);
        }
      }
    });
  }
}

The final version of Promise

Finally, a simple Promise is completed, supporting asynchronous and chain calls of then 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 MyPromise((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);
        }
      }
    });
  }
}

玛尔斯通
486 声望693 粉丝

a cyclist, runner and coder.