An overview of the
It’s been A long time since I last updated the Promise/A+ specification. Previously, a TypeScript Promise library was completed for business purposes. Let’s take A step by step look at how we implement A Promise library that complies with the Promise/A+ specification.
For those of you who are not familiar with the Promise/A+ specification, I suggest you take A look at the previous blog – Front-end basics – the Promise/A+ specification.
The implementation process
First, let’s take a look. In the Promise I implemented, the code consisted of the following parts:
- Global asynchronous function executor
- Constants and Attributes
- Class method
- Class static method
With these four parts, we can make a complete Promise. These four parts are related to each other, so let’s look at one module at a time.
Global asynchronous function executor
In a previous Promiz source code analysis blog POST, I mentioned how we could implement an asynchronous function executor. As we can see from JavaScript execution, if we want to implement asynchronous execution of related functions, we can choose to use macro and micro tasks, which is also mentioned in the Promise/A+ specification. Therefore, below we provide a macro task to implement asynchronous function executor code for your reference.
let index = 0;
if (global.postMessage) {
global.addEventListener('message'.(e) = > {
if (e.source === global) {
let id = e.data;
if (isRunningTask) {
nextTick(functionStorage[id]);
} else {
isRunningTask = true;
try {
functionStorage[id] ();
} catch (e) {
}
isRunningTask = false;
}
delete functionStorage[id];
functionStorage[id] = void 0; }}); }function nextTick(func) {
if (global.setImmediate) {
global.setImmediate(func);
} else if (global.postMessage) {
functionStorage[+ +index] = func;
global.postMessage(index, The '*')}else{ setTimeout(func); }}Copy the code
Using setImmediate, postMessage, and setTimeout to add a macro task, we can see that the above code uses setImmediate, postMessage, and setTimeout to perform a single function.
Constants and Attributes
Having said how to execute asynchronous functions, let’s take a look at the constants and attributes involved. Before we can implement the Promise, we need to define constants and class attributes that will be used to store the data later. Let’s take a look at them one by one.
constant
First of all, there are five states of Promise, which we need to define with constants, as follows:
enum State {
pending = 0,
resolving = 1,
rejecting = 2,
resolved = 3,
rejected = 4
};
Copy the code
These five constants correspond to each of the five states in Promise, which I’m sure you’ll understand from the name, but we won’t go into that.
attribute
In a Promise, we need properties to store the data state and subsequent Promise references as follows:
class Promise {
private _value;
private _reason;
private _next = [];
public state: State = 0;
public fn;
public er;
}
Copy the code
Let’s explain the attributes one by one:
_value
Is used to store the current value in the Resolved state._reason
In the Rejected state, it is used to store the current cause._next
Is followed by the current Promisethen
Function references.fn
Represents in the current Promisethen
The first callback function of the.er
Represents in the current Promisethen
Method’s second callback function (i.ecatch
The first argument to, let’s seecatch
The implementation method can be understood.
Class method
Having looked at constants and class properties, let’s look at static methods of the class.
Constructor
First, if we want to implement a Promise, we need a constructor to initialize the original Promise. The specific code is as follows:
class Promise {
constructor(resolver?) {
if (typeofresolver ! = ='function'&& resolver ! = =undefined) {
throw TypeError()}if (typeof this! = ='object') {
throw TypeError()}try {
if (typeof resolver === 'function') {
resolver(this.resolve.bind(this), this.reject.bind(this)); }}catch (e) {
this.reject(e); }}}Copy the code
From the Promise/A+ specification, we know that if the resolver exists and is not A function, then we should throw an error; Otherwise, we should pass the resolve and reject methods as parameters to resolver.
resolve && reject
So what do resolve and Reject do? These two methods are essentially used to allow the current Promise to switch state, from a pending state to a considerations or reality state. Let’s look at the code in detail:
class Promise {
resolve(value) {
if (this.state === State.pending) {
this._value = value;
this.state = State.resolving;
nextTick(this._handleNextTick.bind(this));
}
return this;
}
reject(reason) {
if (this.state === State.pending) {
this._reason = reason;
this.state = State.rejecting;
this._value = void 0;
nextTick(this._handleNextTick.bind(this));
}
return this; }}Copy the code
As you can see from the code above, when resolve or Reject is triggered, we change the state of the current Proimse and asynchronously invoke the _handleNextTick method. This change in state signifies that the current Promise has changed from its pending state to a resolving or reality state, and the corresponding _value and _reson represent the data that the previous Promise will pass to the next Promise.
So, what does this _handleNextTick method do? This method simply handles the fn and er callbacks passed in by the then function that follows the current Promise.
then && catch
Before we look at _handleNextTick, let’s look at the implementation of the then and catch functions.
class Promise {
public then(fn, er?) {
let promise = new Promise(a); promise.fn = fn; promise.er = er;if (this.state === State.resolved) {
promise.resolve(this._value);
} else if (this.state === State.rejected) {
promise.reject(this._reason);
} else {
this._next.push(promise);
}
return promise;
}
public catch(er) {
return this.then(null, er); }}Copy the code
Since a catch call is an alias for a THEN function, we will discuss only the THEN function.
When the THEN function executes, we create a new Promise and then save the two callbacks passed in with the properties of the new Promise. Then, judge the current state of the Promise. If it is resolved or Rejected, call the resolve or reject method in the new Promise immediately. Let’s pass the current Promise’s _value or _reason to the next Promise and trigger a state change for the next Promise. If the state of the current Promise is still pending, the newly generated Promise will be saved. After the state of the current Promise changes, a new Promise change will be triggered. Finally, we return an instance of the Promise.
handleNextTick
Now that we’ve looked at the then function, we can look at the handleNextTick function that we mentioned.
class Promise {
private _handleNextTick() {
try {
if (this.state === State.resolving && typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1; }}catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}
// if promise === x, use TypeError to reject promise
// If a promise and x refer to the same object, reject the promise with TypeError as the reason
if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError(a);this._value = void 0;
}
this._finishThisTypeScriptPromise(); }}Copy the code
Let’s start with a simple version of the _handleNextTick function to help you quickly understand the Promise main flow.
After triggering the _handleNextTick function asynchronously, we will determine the state of the current user. If the current Promise is a resolving state, we will call the FN function that we set for the new Promise when we call the THEN function. And if the current Promise was reality, we would call the ER function.
The getThis method mentioned above is used to get a specific this value, but the specification requirements will be covered later.
By executing the two synchronized FN or ER functions, we can get the value of the current Promise after it executes the incoming callback. Before we execute fn or er, the values we store in _value and _reason are the ones passed from the previous Promise. Only after the fn or er functions are executed are the values in _value and _reason that we pass to the next Promise.
You might be surprised to see that our “this” points to the same Promise, but why does our “this” point to the new Promise and not the old Promise?
There’s another way to look at it: Does our current Promise result from a previous Promise? If this is the case, we can understand that the fn and er functions were set when the previous Promise produced the current Promise.
So you might say, well, where did we get fn and ER for our first Promise?
So we need to take a closer look at this logic. Let’s just discuss the case where the first Promise is pending, and the rest of the cases are pretty much the same. The first Promise is undefined because the fn and er parameters are not set in the previous Promise. So in the above logic, we have been ruled out by this kind of situation, directly into the _finishThisTypeScriptPromise function.
One thing to note here is that some people might think that when we call the two callback functions fn and er passed in by then, the current Promise ends. This is not the case. We get the return value of fn or er and pass it to the next Promise. The last Promise would have ended. We can see _finishThisTypeScriptPromise function about this logic.
finishThisTypeScriptPromise
_finishThisTypeScriptPromise function code is as follows:
class Promise {
private _finishThisTypeScriptPromise() {
if (this.state === State.resolving) {
this.state = State.resolved;
this._next.map((nextTypeScriptPromise) = > {
nextTypeScriptPromise.resolve(this._value);
});
} else {
this.state = State.rejected;
this._next.map((nextTypeScriptPromise) = > {
nextTypeScriptPromise.reject(this._reason); }); }}}Copy the code
We can see from the _finishThisTypeScriptPromise function, we need pass to the next in the Promise after _value or _reason, one by one with the map method call us the Promise of the new generation saved instance, Calling its resolve method, we again triggered the Promise state to switch from pending to resolving or rejecting.
At this point, we’ve seen the full life cycle of a Promise, from its inception to its end. Let’s take A look at how some of the branching logic mentioned in the Promise/A+ specification works.
The value passed in the previous Promise was an instance of Thenable
First, let’s take a look at what a Thenable instance is. Thenable instances refer to objects that have then functions in their properties. Promise is a special Thenable object.
Next, for the convenience of explanation, we will use Promise to replace Thenable for explanation. Other Thenable classes can be analyzed by referring to similar ideas.
If we have an instance of a Promise in the _value passed to us, we must wait for the incoming Promise state to transition to resolved before the current Promise can proceed. That is, when we get a non-Thenable return value from the incoming Promise, We can use this value to call fn or er methods on properties.
So, how do we get the return value of this Promise passed in? There’s actually a pretty neat trick to use in promises: Since the incoming Promise has a THEN function (as defined by Thenable), we call the THEN function, passing the fetch _value in the first callback function fn, which triggers the current Promise to continue. If the second callback er is fired, the _reason obtained in er is used to reject the current Promise. The specific judgment logic is as follows:
class Promise {
private _handleNextTick() {
let ref;
let count = 0;
try {
// Check whether this._value passed in is a thanable
// check if this._value a thenable
ref = this._value && this._value.then;
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
return this._handleNextTick();
}
if (this.state ! == State.rejecting && (typeof this._value === 'object' || typeof this._value === 'function') && typeof ref === 'function') {
// add a then function to get the status of the promise
// Add a then function after TypeScriptPromise to determine the state of the original TypeScriptPromise
try {
ref.call(this._value, (value) = > {
if (count++) {
return;
}
this._value = value;
this.state = State.resolving;
this._handleNextTick();
}, (reason) = > {
if (count++) {
return;
}
this._reason = reason;
this.state = State.rejecting;
this._value = void 0;
this._handleNextTick();
});
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._handleNextTick(); }}else {
try {
if (this.state === State.resolving && typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1; }}catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}
this._finishThisTypeScriptPromise(); }}}Copy the code
promise === value
In the Promise/A+ specification, A TypeError is required to reject the current Promise if the _value returned is equal to the user itself. So we need to add the following code to _handleNextTick:
class Promise {
private _handleNextTick() {
let ref;
let count = 0;
try {
// Check whether this._value passed in is a thanable
// check if this._value a thenable
ref = this._value && this._value.then;
} catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
return this._handleNextTick();
}
if (this.state ! == State.rejecting && (typeof this._value === 'object' || typeof this._value === 'function') && typeof ref === 'function') {
// add a then function to get the status of the promise
// Add a then function after TypeScriptPromise to determine the state of the original TypeScriptPromise. }else {
try {
if (this.state === State.resolving && typeof this.fn === 'function') {
this._value = this.fn.call(getThis(), this._value);
} else if (this.state === State.rejecting && typeof this.er === 'function') {
this._value = this.er.call(getThis(), this._reason);
this.state = 1; }}catch (e) {
this.state = State.rejecting;
this._reason = e;
this._value = void 0;
this._finishThisTypeScriptPromise();
}
// if promise === x, use TypeError to reject promise
// If a promise and x refer to the same object, reject the promise with TypeError as the reason
if (this._value === this) {
this.state = State.rejecting;
this._reason = new TypeError(a);this._value = void 0;
}
this._finishThisTypeScriptPromise(); }}}Copy the code
getThis
The Promise/A+ specification states that when we call the fn and er callback functions, this has A limited reference. In strict mode, this should be undefined; In loose mode, the value of this should be global.
Therefore, we also need to provide a getThis function to handle the above case. The specific code is as follows:
class Promise{... }function getThis() {
return this;
}
Copy the code
Class static method
We have implemented a Promise class that meets basic functionality by using the class methods described above and some logical handling of specific branches. So, let’s look at some of the standard apis provided in ES6 and see how we can implement them. The specific API is as follows:
- resolve
- reject
- all
- race
Let’s take a look at one method at a time.
resolve && reject
First let’s look at the simplest resolve and reject methods.
class Promise {
public static resolve(value?) {
if(TypeScriptPromise._d ! = =1) {
throw TypeError(a); }if (value instanceof TypeScriptPromise) {
return value;
}
return new TypeScriptPromise((resolve) = > {
resolve(value);
});
}
public static reject(value?) {
if(TypeScriptPromise._d ! = =1) {
throw TypeError(a); }return new TypeScriptPromise((resolve, reject) = >{ reject(value); }); }}Copy the code
As you can see from the code above, the resolve and Reject methods basically use the internal constructor method directly to build promises.
all
class Promise {
public static all(arr) {
if(TypeScriptPromise._d ! = =1) {
throw TypeError(a); }if(! (arrinstanceof Array)) {
return TypeScriptPromise.reject(new TypeError());
}
let promise = new TypeScriptPromise();
function done() {
// Count how many typescriptPromises are left unfinished
// count the unresolved promise
let unresolvedNumber = arr.filter((element) = > {
return element && element.then;
}).length;
if(! unresolvedNumber) { promise.resolve(arr); } arr.map((element, index) = > {
if (element && element.then) {
element.then((value) = > {
arr[index] = value;
done();
returnvalue; }); }}); } done();returnpromise; }}Copy the code
Let’s talk about the basic idea of the all function based on the above code.
First, we need to create a new Promise to return, so that the fn and ER callbacks of the new Promise can be set when the user calls the then function for subsequent logical processing.
Then, how do we get the value of each Promise in the Promise array above? The method is simple, as we described earlier: we call each Promise’s then function to get the current Promise value. Also, with each Promise completion, we checked to see if all promises were completed, and if they were, the state of the new Promise was switched from pending to considerations or reality.
race
class Promise {
public static race(arr) {
if(TypeScriptPromise._d ! = =1) {
throw TypeError(a); }if(! (arrinstanceof Array)) {
return TypeScriptPromise.reject(new TypeError());
}
let promise = new TypeScriptPromise();
function done(value?) {
if (value) {
promise.resolve(value);
}
let unresolvedNumber = arr.filter((element) = > {
return element && element.then;
}).length;
if(! unresolvedNumber) { promise.resolve(arr); } arr.map((element, index) = > {
if (element && element.then) {
element.then((value) = > {
arr[index] = value;
done(value);
returnvalue; }); }}); } done();returnpromise; }}Copy the code
The idea of Race is basically the same as all. It’s just that we’re dealing with functions differently. When we detect that one of the promises in the array has shifted to the resolve or Rejected state (as determined by the non-THEN function), we immediately switch the state of the newly created Promise sample from pending to resolving or rejecting.
conclusion
We introduced the asynchronous function executor, constant and attribute, class method, class static method of Promise one by one, so that we have a deep understanding and cognition of the construction and declaration cycle of the whole Promise. A few key points and details to be aware of throughout the development have also been explained above. All you need to do is follow this idea and compare the Promise/A+ specification to complete A compliant Promise library.
Finally, provide A Promise/A+ testing tool that you can use to test full compliance with the entire Promise/A+ specification once you implement your Promise. Of course, if you want to use my off-the-shelf code, you are welcome to use my code Github/ typescript-Proimse.