Use JavaScript to implement a simple Promise step by step, supporting asynchronous and then chained calls.
Translated and organized from
Medium: Implementing a simple Promise in Javascript – by Zhi Sun
preface
Promise is often encountered in front-end interviews and daily development. And in many interviews these days, promises are often written by hand.
Next, you’ll use JavaScript to implement a simple Promise step by step that supports asynchronous and then chained calls.
Analysis of the Promise
The Promise object is used to represent the final completion (or failure) of an asynchronous operation and its resulting value, and is often used to implement asynchronous operations.
State of Promise
Promise has three states:
-
pending
The initial state
-
fulfilled
Status after successful execution
-
rejected
The state after the execution failed
Promise state can only be changed from pending to fulfilled or from pending to rejected. The changing process of the state of a Promise is called settling. Once the state is changed, it will not be changed again.
Arguments to the Promise constructor
The Promise constructor takes a function argument executor, which takes two arguments:
- resolve
- reject
Fulfilling will change the Promise state from pending to fulfilled and trigger the successful callback function onFulfilled in the then method.
Performing Reject changes the Promise status from pending to Rejected and triggers the failure callback function onRejected in the Then method.
Callback function parameters in the Then method
The then method takes two arguments:
-
onFulfilled
The call back to the function succeeds, receiving a single argument, the value passed in the resolve function
-
onRejected
Failed callback function, receiving a single argument, reject, the value passed in the function
If the Promise state becomes fulfilled, the successful callback function onFulfilled is performed; If the Promise status becomes Rejected, the failure callback function onRejected is executed.
Realize the Promise
Basic Promise
First, this constructor receives a function executor, which in turn takes two arguments, the resolve and reject functions.
Therefore, you need to create the resolve and reject functions in your 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); }}}
Second, Promise executes the corresponding callback function, based on the state. The initial state is pending, and when resolve, the state changes from pending to fulfilled. When Reject, the state 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); }}}
When the Promise status changes, the corresponding callback function in the Then method is fired. If the state is changed from Pending to Fulfilling, then the success callback will be triggered. If the state is changed from Pending to Rejected, then 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); }}}
Then 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
);
However, if you test it with the following code, you’ll 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)
);
This is because the Promise is still pending when the Then method is called. Neither onFulfilled nor OnRejected callback functions are executed.
Therefore, the next step is to support asynchrony.
Support for asynchronous Promises
To support asynchrony, you need to save the onFulfilled and onRejected callbacks and execute the corresponding callbacks as soon as the Promise status changes.
⚠ : There is a detail to be noted here, that isonFulfilledCallbacks
andonRejectedCallbacks
Is an array, because a Promise may be called multiple times, so there will be multiple callbacks.
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 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
);
However, if you test with the following code, you will find that the 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
This is because the first then method does not return any value, but then methods are called continuously.
Therefore, the next step is to implement a Then chained call.
supportthen
Chain call Promise
To support then chained calls, then methods need to return a new Promise.
Therefore, you need to transform the Then method to return a new Promise and then execute the resolve or reject function of the new Promise after the onFulfilled or onRejected callback of the previous 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 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
);
However, if you switch to the following code test, you will find 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)
);
This is because, fulfilled/rejected, you will simply pass the values returned by the onFulfilled/ rejected callback into the resolve/reject function. There is no consideration for cases where onFulfilled/ OnRejected will return a new Promise, so the successful callback of the second Then method outputs the Promise returned by the successful callback of the previous Then method. Therefore, the next step is to solve this problem.
First of all, you can change the above test code into another way of writing, convenient to comb the train of thought.
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’s P1 constructed by new
-
The second Promise
That is the P2 returned by calling the Then method
-
The third Promise
That is the p3 returned in the success callback function argument of the p1. Then method
The problem is that P3 is still pending when P2’s then method is called.
When p2. Then is used to print the resolve/reject value in p2, it is necessary to wait for the state of p3 to change and pass the changed value into the resolve/reject value in p2. In other words, the sequence of the three Promise state changes 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 Promise
Finally, a simple Promise is completed, supporting asynchronous and then chained 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); }}}); }}