This article will implement A simplified Promise library based on Promises/A+ specification that implements the apis commonly used in native Promise objects. The full version of the Proimse library code has been posted to personal Github, and the simple Promise library will be implemented step by step from 0
1. Original version
First of all, the basic features of promise can be summarized as follows:
- The new constructor creates a Promise instance object. The new constructor passes in a function executor that takes resolve and reject as parameters of two method types
- 2 New Promise After the instance object is obtained, the default state of the object is pending. This object will become a pity after you call ‘resolve’ in executor, and the object will become ‘Rejected’ after you call ‘Reject’
- 3 When resolve is called, a value must be accepted as the value of the Promise instance object and cannot be changed
- 4 When you call REJECT, you must receive an unchangeable reason value as the reason of the Promise instance object
- 5 If an error is raised during the Executor function, the Promise instance changes to the Rejected state and the error is the Reason of the Object
- 6 Once the Promise object changes from pending to the fulfilled or Rejected state, the state of the Promise object cannot be changed
- 7 The promise instance object has the then method, which can accept two parameters onFulfilled and onRejected(which is usually a function). The functions need to be executed when the Proimse object changes to the fulfilled state and the functions need to be executed when the Promise object changes to the Rejected state
Based on these features, we can create a basic Promise constructor
const PENDING = 'PENDING';
const FULFILLED = 'FULFILLED';
const REJECTED = 'REJECTED';
function Promise(executor) {
this.status = PENDING;
this.value = undefined;
this.reason = undefined;
const resolve = value= > {
if (this.status === PENDING) {
this.value = value;
this.status = FULFILLED; }};const reject = reason= > {
if (this.status === PENDING) {
this.reason = reason;
this.status = REJECTED; }};try {
executor(resolve, reject);
} catch(err) { reject(err); }}Promise.prototype.then = function then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason); }}Copy the code
2. Add asynchronous features
This is the most basic version, and only works if the executor that is passed in a New Promise synchronously executes resolve or Reject. Consider that if the executor uses an asynchronous call to resolve or Reject, such as ajax, As follows:
let p = new Promise((resolve) = > {
// You can use setTimeout or call resolve/ Reject in a callback after making an Ajax request
setTimeout(() = > {
resolve(100)},1000)}); p.then(res= > console.log(res));
Copy the code
The core logic is actually the publish-subscribe model. The two parameters received by the THEN method are stored first (subscribe), and the previously saved functions are taken out and executed successively (publish) when the state of the Promise instance object changes. Therefore, we add publish-subscribe logic on the previous version
function Promise(executor) {
const resolve = value= > {
if (this.status === 'PENDING') {
this.value = value;
this.status = FULFILLED;
// Release the depressing state callback functions saved before and execute them one by one
if (this.onFulfilledCallbacks.length) {
this.onFulfilledCallbacks.forEach(cb= > cb(this.value)); }}};const reject = reason= > {
if (this.status === 'PENDING') {
this.reason = reason;
this.status = REJECTED;
// Publish the callback function of the Rejected state that was saved before and execute it one by one
if (this.onRejectedCallbacks.length) {
this.onRejectedCallbacks.forEach(cb= > cb(this.reason)); }}}; };Promise.prototype.then = function then(onFulfilled, onRejected) {
if (this.status === FULFILLED) {
onFulfilled(this.value);
}
if (this.status === REJECTED) {
onRejected(this.reason);
}
// Subscribe to store the two callback functions when the promise state does not change when the then function is executed
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected); }}Copy the code
3. Return value of the then method
Now that our promise has support for asynchronous logic in executor, we need to consider the return value of the THEN method that can be used to chain-call the THEN method. Let’s start with the description of the THEN method in PromiseA+
Let’s explain these features:
- 2.2.7
then
The method needs to return a new promise object promise2 - 2.2.7.1 If the ondepressing and onRejected methods in promise1. Then return a value x, then pass X and the upcoming return promise2 into the Promise Resolution Procedure method. This method changes the state of promise2 based on the value of x
- 2.2.7.2 When an exception is thrown during the onFulfilled or onRejected, promise2 should become the Rejected state and the exception should be the reason
- 2.2.7.3 If promise1 is a depressing state, but the ondepressing passed in by promise1. Then is not a function, The promisE2 that needs to be returned will become the depressing state and its value will be the same as the value of promise1
- 2.2.7.4 If promise1 is in the Promise1 state and the onRejected sent by promise1. Then is not a function, the returned promise2 will become Promise1 and its reason value is the same as that of Promise1
Promises/A+ Specification also Promises/A+ specification (3.1 in 3.Notes)
3.1 Here “platform code” means engine, environment, and promise implementation code. In practice, this requirement ensures that onFulfilled and onRejected execute asynchronously, after the event loop turn in which then is called, This can be implemented with either a “macro-task” mechanism such as setTimeout or setImmediate, Or with a “micro-task” mechanism such as MutationObserver or process.nexttick. Since the promise implementation is considered platform code, It may itself contain a task-scheduling queue or “trampoline” in which the handlers are called.
This is a big pity. OnFulfilled and onRejected need to be implemented asynchronously in the callback of the promise. For example, use setTimeout or setImmerdiate macro tasks or MutationObserver and process.nextTick microtasks to handle these two functions.
So we are going to modify the previous THEN method to implement the points mentioned in the specification above
Promise.prototype.then = function then(onFulfilled, onRejected) {
// The newly created promise2 is the promise object returned by the then method
let promise2 = new Promise((resolve, reject) = > {
if (this.status === FULFILLED) {
// 3.1 onFulfilled and onRejected are implemented asynchronously
setTimeout(() = > {
try {
2.2.7.1 Pass the return value X and promise2 into the Promise Resolution Procedure method (we define the method named resolvePromise)
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
// 2.2.7.2 If an exception is thrown during the Configuration, the promise2 state changes to Rejected and the exception is the reason of the promise2reject(err); }},0);
}
if (this.status === REJECTED) {
setTimeout(() = > {
try {
// 2.2.7.1 Pass the return values x and promise2 of onRejected into the Promise Procedure method (we define the method as resolvePromise)
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch(err) {
// 2.2.7.2 If an exception is thrown during the Configuration, the promise2 state changes to Rejected and the exception is the reason of the promise2reject(err); }},0);
}
if (this.status === PENDING) {
this.onFulfilledCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
} catch(err) { reject(err); }},0)});this.onRejectedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
} catch(err) { reject(err); }})}); }});2.2.7 Returns a new Promise object
return promise2;
}
Copy the code
4. Promise Resolution Procedure
Then we’ll write the core method that determines the state of the promise2 object to be returned after calling the promise1. Then method.
The core logic of this method is to invoke the resolve and Reject methods of promise2 respectively to change the state of promise2 according to the ondepressing and the return value X of onRejected implemented in promise1. Then. So how does PromiseA+ explain this methodThis is a brief outline of what these specifications say. X represents the ondepressing of promise1 and the return value of the onRejected method. Promise2 represents the new promise object returned by the then method
- When x and promise2 point to the same object, an error is thrown and no further execution is performed
- If x is a non-object and a function (which is a normal value), set promise2 to the depressing state and value to X
- If x is a promise object, if it is pending, it will wait for its state to change. Therefore, call x. Teng method. If ondepressing is performed, the return promise2 will also be fulfilled, and the value of X will be inherited. If onRejected is executed, promise2 is returned as Rejected and inherits the reason of X
- If an exception is thrown when executing X. Chen, set promise2 to the Rejected state and the exception object is its reason
Next we implement the Promise Resolution Procedure, which we declare as a resolvePromise
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
/** * 2.3.1 If promise2 and x point to the same object, Promise1 = new Promise(resolve => resolve(1)); let promise1 = new Promise(resolve => resolve(1)); * let promise2 = promise1. Then (() => {return promise2}
return reject(new TypeError('Chaining cycle detected for promise'));
}
if ((typeof x === 'object'&& x ! =null) | |typeof x === 'function') {
// 2.3.3 x is an object or function, then can be a promise
try {
// 2.3.3.1 Let then be x.teng
let then = x.then;
if (typeof then === 'function') {
If then is a function,call it with x as this, first argument resolvePromise, and second argument rejectPromise
If x is an object or function that has a THEN attribute and the then attribute is a function, it is considered a Promise object
then.call(x, y= > {
If/when resolvePromise is called with a value y, run [[Resolve]](resolvePromise, y)
// Where X. Chen performs ondepressing method, the y may still be a promise, so we need to recursively call resolvePromise to confirm whether y is a promise(if it is a promise, we need to wait for the state change) or an ordinary value. To determine the state of the promise2 that is returned
resolvePromise(promise2, y, resolve, reject);
}, r= > {
// 2.3.3.3.2 If/when rejectPromise is called with a reason r, reject promise with rreject(r); })}else {
If then is not a function, fulfill promise with x
// This will be a big pity if THEN is not a function, which means that THEN is only a common attribute value, and x is only a common objectresolve(x); }}catch(err) {
// If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.
// If an exception is thrown during the then fetch, the promise2 state is set to Rejectedreject(err); }}else {
Fulfill (fulfill) fulfill (fulfill) iF x is not an object or function fulfill (fulfill) promise with x
// x is neither an object nor a function, so it must be a normal valueresolve(x); }}Copy the code
Now that the general logic of the resolvePromise method is complete, there is one more detail that needs to be mentioned in the PromiseA+ specification:
2.3.3.3.3 If both resolvePromise and rejectPromise are called, or multiple calls to the same argument are made, the first call takes precedence, and any further calls are ignored.
This means that if X is a Promise object and both resolve and Reject are called, or resolve or Reject are called multiple times, then only the first call is valid and all subsequent calls should be ignored.
We know that our own promises have an if (this.status === PENDING) determination in resolve and Reject, which means that if resolve or Reject has been called once before, Repeated calls will be intercepted by the if statement and return directly, so why add such a specification here?
Because x here will not only be the promise object we implement, but also the promise object created by third-party libraries such as Bluebird and Q. We cannot guarantee that the promise object of these third-party libraries will add a layer of judgment to resolve and Reject like our own libraries. So we need to handle the behavior of these third-party library promise objects. Even if the third-party library promise calls resolve and reject repeatedly, our code will only respond once and will not do any more processing, so we need to modify the resolvePromise method. Add a flag bit to avoid repeated changes in the state of the Promise object
function resolvePromise(promise2, x, resolve, reject) {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'));
}
if ((typeof x === 'object'&& x ! =null) | |typeof x === 'function') {
// Add a flag to indicate whether the state of the Promise object has changed
let called = false;
try {
let then = x.then;
if (typeof then === 'function') {
then.call(x, y= > {
// If the status has changed, return does not perform subsequent operations
if (called) return
called = true;
resolvePromise(promise2, y, resolve, reject);
}, r= > {
// If the status has changed, return does not perform subsequent operations
if (called) return
called = true; reject(r); })}else{ resolve(x); }}catch(err) {
if (called) return;
called = true; reject(err); }}else{ resolve(x); }}Copy the code
5. State transparent transmission of Promise objects is supported
One feature missing from the code above is what the Promise specification says about parameters passed in the.then method
- 2.2.7.3 This is a big pity If onprogressively is not a function and promise1 is a big pity. promise2 must be fulfilled with the same value as promise1.
- 2.2.7.4 If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.
This will be a depressing state if the first parameter is not passed or the first parameter is not a function when calling promise1. Then, the return promise2 will also become a depressing state. And the value of promise2 is the value of promise1. If promise1 is the Rejected state and the 2nd parameter is not passed when promise1. Then or the 2nd parameter is not a function, the returned promise2 becomes the Rejected state and its reason is the reason of promise1. Here’s an example:
let promise1 = new Promise(resolve= > resolve(123));
promise1.then().then().then(res= > {
console.log(res); // It is 123
})
Copy the code
When the then method is called without passing functional parameters as specified, the state of the promise is passed through until the THEN method that passes function parameters is encountered.
So how do we implement this functionality? So this is the same thing as this
let promise1 = new Promise(resolve= > resolve(123));
promise1.then(res= > res).then(res= > res).then(res= > {
console.log(res);
})
Copy the code
The core logic is that when we realize that the parameters passed by the THEN method are not functions, we will implement the function content ourselves, which will be fulfilled gradually by returning the accepted value (the promise1 value). The newly generated promise object, after passing through the resolvePromise method, will take the return value as the value of the new promise
The same goes for onRejected. If the function is not passed in, we will implement the function content ourselves. The function content will throw the previously received content as an Error, and the next received promise will become the Rejected state and the reason is the Error
So we implement this logic in the promise.then method
Promise.prototype.then = function then(onFulfilled, onRejected) {
// This is a big pity. // If the onFulfilled is not a function, create a function yourself and return the value received
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : (value) = > value;
// If onRejected is not a function, create a function that throws the received content as an Error
onRejected = typeof onRejected === 'function' ? onRejected : (err) = > {throwerr; }let promise2 = new Promise(() = > {
// ...
});
return promise2;
}
Copy the code
At this point, a simple Promise library is complete, and we’re ready to experiment with it. Let’s write an example:
let p = new Promise(function(resolve, reject) {
setTimeout(() = > {
resolve(123);
});
});
let p2 = p.then(res= > {
console.log('resolve: ', res);
return 456;
}, err= > {
console.log('rejected: ', err);
});
p2.then().then().then().then(res= > {
console.log('p2 fulfilled: ', res);
}, err= > {
console.log('p2 failed: ', err);
})
Copy the code
The final output result is resolve: 123 and P2 depressing: 456
The Promise library implementation is not finished yet. We still need to use A testing tool to check whether the Promise library fully complies with Promises/A+. We also need to implement the Promise. This will be done in Promises/A+ Promise(Part 2)
The resources
- Promise use tutorial
- Promises/A + specification
- Promise library complete code