As we worked, we used promises to implement some of the features, but we never knew why. Therefore, this article mainly explores the implementation principle of Promise, but there is no need to elaborate on the usage, which is to implement a Promise step by step.
1. Basic structure of Promise
const p1 = new Promise((resolve, reject) => { // ... Resolve (value); resolve(value); } else { reject(error); }})Copy the code
First, the Promise’s constructor accepts a callback function as an argument.
Second, the callback takes two arguments, resolve and reject, and executes resolve or reject selectively in the body.
Finally, once the Promise instance is generated, we define the resolve and Reject functions that need to be performed in the THEN method.
p1.then(() => {
// success
}, () => {
// failure
})
Copy the code
If resolve(value) is executed, the state changes from incomplete to successful, and the first method of then is executed.
When reject(error) is executed, the state changes from “reject” to “failed”, the second method of then is executed, and the state does not change.
So the basic structure of Promise is the above, to analyze its implementation logic.
Implementation logic
- This is a big pity. I will define three states of Promise, which are pending, successful and rejected.
- We need to create a class whose constructor takes a function (executor) as an argument. The internal variables are:
- Status, stores the Promise state;
- Value, a variable that stores error on failure or value on success;
- The function variable of the depressing state, resolveQueue, stores the functions passed in by the THEN method;
- RejectQueue, a function variable in the Rejected state, which stores functions passed in by the then method.
- Execute the executor function, which takes resolve and reject as arguments. Resolve and reject must be inside the executor constructor and executed inside the executor.
- Change the Promise state;
- Accepts a value of any type as an argument, indicating whether the Promise succeeds or fails, and synchronizes to value;
- Execute resolveQueue or rejectQueue.
- There needs to be a THEN method, and the THEN method receives two parameters, one is fulfilled when the pity is fulfilled, and the other is fulfilled when the rejected function is fulfilled. The then method stores the two function variables to resolveQueue and rejectQueue, respectively, and executes them after the status changes
So the code implementation is as follows:
Const PENDING = 'PENDING' const depressing = 'depressing' const PENDING = 'depressing' Const REJECTED = 'REJECTED' class ourPromise {constructor(executor) {// This points to the instance object // Status = PENDING ResolveQueue = [] this.rejectQueue = [] let resolve = (val)=>{ if(this.status ! == PENDING) return; This. Value = val enclosing status = FULFILLED this. ResolveQueue. ForEach (callback = > {callback (val)})} / / function to be executed by the failure to reject = (err) =>{ if(this.status ! == PENDING) return; this.value = err this.status = REJECTED this.rejectQueue.forEach(callback=>{ callback(err) }) } // Execute executor(resolve,reject)} then(errledFun,rejectedFun){execute executor(resolve,reject) then(errledFun,rejectedFun){ this.resolveQueue.push(fulFilledFun) this.rejectQueue.push(rejectedFun) } }Copy the code
Test the
const p1 = new ourPromise((resolve,reject)=>{ setTimeout(()=>{ resolve('success') },500) }) p1.then((res)=>{ Console. log(res)}) // Output after 500ms // successCopy the code
The basic Promise is implemented, so let’s tease out how it works:
- Create an ourPromise instance, passing in an argument that is a function;
- Enter the constructor for initialization, and finally execute the argument function passed in. Inside is the asynchronous function, insert the macro task, and continue to execute the synchronous statement.
- The then method is passed a callback that executes on success, at which point the resolveQueue changes, that is, the Promise instance now changes to
status = PENDING
value = null
resolveQueue = [
(res)=>{
console.log(res)
}
]
rejectQueue = []
Copy the code
As you can see, then does not execute any statement, but simply overwrites the attributes of the current instance.
- After the synchronization task is complete, switch to the macro task
resolve('success')
, the following function is called
let resolve = (val)=>{
this.value = val
this.status = FULFILLED
this.resolveQueue.forEach(callback=>{
callback(val)
})
}
Copy the code
Why are the two functions passed in by the then method here stored in an array?
The THEN method can be called multiple times by the same Promise instance to support it
Such as:
const p1 = new ourPromise((resolve,reject)=>{ setTimeout(()=>{ resolve('success') },500) }) p1.then((res)=>{ Console. log(res)}) p1.then((res)=>{console.log(' second then method '+res)}Copy the code
2. Implementation of then method
ES6’s then method for promises
p1.then(res,rej)
Copy the code
The THEN method supports the following functions:
- The res, rej arguments are optional, but must be functions
- When p1 becomes successful, res and rej must be called respectively. The parameters are value and ERR passed in resolve(Value) and Reject (err)
- It cannot be called until the p1 state changes
- The number of calls cannot exceed one
- The THEN method can be called multiple times by the same Promise instance
- When P1 is successful, all res need to be called back in the order in which they were registered
- When P1 fails, all REJs need to be called back according to their registration order
- The then method returns a value as a Promise object that supports chained calls
Now, let’s implement the above three functions, the second of which was implemented in section 1 above. So let’s implement the core chain call of Promise.
In learning about promises, I often come across examples that test the chain calls of Promise’s then method.
const p1 = new Promise( (resolve, reject) => { setTimeout(() => { resolve(1) }, 500); } ) p1 .then(res => { console.log(res) return 2 }) .then(res => { console.log(res) return 3 }) .then(res => { Console. log(res)}) // sequential output // 1 // 2 // 3Copy the code
There are a few things you can clearly see in this code:
- P1.then () still supports then methods, indicating that p1.then() is also a Promise object.
- The p1.then() method returns the Promise object, but the successful callback has no resolve statement inside it, just return 2. So how is it handled internally? When does the Promise object returned by p1.then() change state?
The then method returns a Promise instance
So, the first thing to do is to make it return a Promise object, which is to change the then method.
then(fulFilledFun,rejectedFun){
return new ourPromise((resolve,reject)=>{
this.resolveQueue.push(fulFilledFun),
this.rejectQueue.push(rejectedFun)
})
}
Copy the code
After rewriting the above, we can successfully return a Promise, and we can support chained THEN methods with no errors, but we can’t chain the function we wrote.
Based on this, the registration timing of each Promise instance is shown below. In general, when the THEN method is called, not only is a new Promise instance created, but the resolveQueue of the previous instance is changed to be executed when the state changes. Where the Promise instance created by p1.then().then().then().then(). Its resolveQueue instance remains unchanged and remains [].
Chain executes the then method
Now for the second point, the chain executes the function we wrote in the then method. In other words, how to ensure that the resolvequeues of each Promise instance are executed sequentially.
As a result of the chained execution example, the return content of the first THEN () method is retrieved by the function inside the second THEN () method. After rewriting the above, it is actually:
- When p1 instance changes to the successful state, the callback function resolve() is executed. Resolve internally executes resolveQueue, whose length is 1.
- The resolveQueue of P1 is executed, and the successful callback function of p1.then() is executed immediately, and so on.
Therefore, the key point is immediate execution, that is, the execution of the former resolveQueue and the change of the state to fullfiled are bound together. That is, the successful and failed callbacks passed in by the THEN () method need to be rewrapped.
The then() method continues rewriting
Return new ourPromise((resolve,reject)=>{// If the wrapper is successful, the callback must be const ReturnV let returnV = fulledFun (value) // Set ourPromise state to be fulfilled, which is a big pity, Const rejectFun = value =>{let returnV = rejectedFun(value) resolve(returnV) Push (resolveFun) this.rejectQueue.push(rejectFun)})} // Change the instance resolveQueue this.resolvequeue.push (resolveFun) this.rejectQueue.push(rejectFun)}Copy the code
After that, you can successfully print 1,2,3 for the initial example. The registration process has been described in the previous section to sort out the process after registration, as shown in the figure.
After rewriting, the resolveQueue property for each instance registered is shown. Now follow the figure to sort out the operation process after registration:
- Resolve (1) execute, execute the successful callback function of p1 instance;
- The state is set to depressing, and the corresponding resolveQueue will be executed. The parameter value is 1, and the corresponding ledFun will be executed. The return value is 2.
- To resolve(2), execute the successful callback function of the p1.then() instance;
- The state is set to depressing, and the corresponding resolveQueue will be executed. The parameter value is 2, and the corresponding ledFun will be executed. The return value is 3.
- To resolve(3), execute the successful callback function of p1.then().then();
- The state is set to depressing, and the corresponding resolveQueue will be executed. The parameter value is 3, and the corresponding ledFun will be executed. Print 3 and undefined will be returned.
- Perform resolve(undefined), and the state is fulfilled. The corresponding resolveQueue is empty, and the process ends.
Now that this simple chain call is basically done, let’s consider some details.
Then detail implementation
- The arguments received by the THEN method may not be functions, and error handling is performed without affecting the execution of the next THEN method, i.e., value penetration;
const p1 = new Promise( (resolve, reject) => { setTimeout(() => { resolve(1) }, 500); }) p1.then ().then(res => {console.log(res) return 3}).then(res => {console.log(res)}) // 1 // 3Copy the code
- A callback function in a success or failure state, which might return a Promise object;
const p1 = new Promise( (resolve, reject) => { setTimeout(() => { resolve(1) }, 500); } ) p1 .then(res => { console.log(res) return new Promise((resolve, reject) => { setTimeout(() => { resolve(2) }, 500))}).then(res => {console.log(res) return 3}).then(res => {console.log(res)}) // 1 // 3Copy the code
- The executor function of the Promise instance is a synchronization task;
Const p1 = new Promise((resolve, reject) => { Resolve (1)}) p1.then (res => {console.log(res) return 2}). Then (res => {console.log(res) return 3}) Then (res = > {the console. The log (res)}) output / / / order / 1 / / 2 / / 3Copy the code
- Promise. Resolve (x), Promise. Reject (x), and return a Promise instance.
Promise.resolve(1).then(res=>{console.log(res)}) resolve('2') }) ) ourPromise.reject(1)Copy the code
The code is as follows:
Then (fallledFun,rejectedFun){// Then (fallledFun,rejectedFun){// Then (fallledFun,rejectedFun){ The next THEN method needs to get the value Typeof the previous THEN method. == 'function' ? Typeof rejectedFun = value => value :null // Failed callback function, need to complete the error cause output, and end typeof rejectedFun! == 'function' ? rejectedFun = reason =>{ throw reason } :null return new ourPromise((resolve,reject)=>{ const resolveFun = value =>{ let // If it is a Promise object, execute the then method of the object, Bind the current Promise object's resolve, reject, returnV instanceof ourPromise? returnV.then(resolve,reject) :resolve(returnV) } const rejectFun = value =>{ let returnV = rejectedFun(value) returnV instanceof ourPromise ? Then (resolve,reject) :resolve(returnV)} // This is a big pity. Resolve (reject); reject (reject); reject (reject); RejectFun switch(this.status){case PENDING: this.resolveQueue.push(resolveFun) this.rejectQueue.push(rejectFun) break case FULFILLED: resolveFun(this.value) break case REJECTED: RejectFun (this.value)}})} Static resolve(value){if(value instanceof ourPromise) return value return new ourPromise(resolve => resolve(value)) } static reject(value){ return new ourPromise((resolve,reject) => reject(value)) }Copy the code
Error capture for the THEN method
In the previous part, we didn’t notice that if something was wrong inside the function, for example
Const p1 = new ourPromise((resolve, reject) => {throw new Error(' Error ')}) p1.then(res => {console.log(res)}, Log (err) console.log(err.message)}) // Does not print console.log('aaaa')Copy the code
You can see that executor threw an error, so
constructor(excutor) { // ... Excutor (resolve,reject) try{excutor(resolve,reject)}catch(error){reject(error)}}Copy the code
It is also possible that two successful and failed callbacks passed to the THEN method throw an error
const resolveFun = value =>{
try{
let returnV = fulFilledFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
}
const rejectFun = value =>{
try{
let returnV = rejectedFun(value)
returnV instanceof ourPromise
? returnV.then(resolve,reject)
:resolve(returnV)
}catch(error){
reject(error)
}
}
Copy the code
If an error is thrown in the current system, it will be caught by the next THEN method, Reject.
Complete code after microtask processing for then methods
Many handwritten versions use setTimeout to do asynchronous processing, but setTimeout is a macro task, which contradicts the Promise of a microtask, so I’m going to choose a way to create a microtask to implement our handwritten code.
NextTick (Node side) and MutationObserver (browser side), as mentioned in the Promise A+ specification. Considering the environmental judgment required to take advantage of these two approaches, So we recommend queueMicrotask as another way to create microtasks.
Const PENDING = 'PENDING' const depressing = 'depressing' const PENDING = 'depressing' Const REJECTED = 'REJECTED' class ourPromise {constructor(excutor) {constructor(excutor) {this. Status = PENDING ResolveQueue = [] this.rejectQueue = [] let resolve = (val)=>{if(this.status! == PENDING) return; this.value = val this.status = FULFILLED this.resolveQueue.forEach(callback=>{ callback(val) }) } let reject = (err) =>{ if(this.status ! == PENDING) return; this.value = err this.status = REJECTED this.rejectQueue.forEach(callback=>{ callback(err) }) } // Execute the callback returned by the defined instance, passing in two arguments: try{excutor(resolve,reject)}catch(error){reject(error)}} // then Then (compliant ledFun,rejectedFun){// Check whether the current function is typeof compliant ledFun! == 'function' ? fulFilledFun = value => value :null typeof rejectedFun ! == 'function' ? rejectedFun = reason =>{ throw reason } :null return new ourPromise((resolve,reject)=>{ const resolveFun = value =>{ // QueueMicrotask (()=>{try{let returnV = enabledFun (value) returnV instanceof ourPromise? returnV.then(resolve,reject) :resolve(returnV) }catch(error){ reject(error) } }) } const rejectFun = value =>{ queueMicrotask(()=>{ try{ let returnV = rejectedFun(value) returnV instanceof ourPromise ? returnV.then(resolve,reject) :resolve(returnV) }catch(error){ reject(error) } }) } switch(this.status){ case PENDING: this.resolveQueue.push(resolveFun) this.rejectQueue.push(rejectFun) break case FULFILLED: resolveFun(this.value) break case REJECTED: rejectFun(this.value) } }) } static resolve(value){ if(value instanceof ourPromise) return value return new ourPromise(resolve => resolve(value)) } static reject(value){ return new ourPromise((resolve,reject) => reject(value)) } }Copy the code