preface
In this article, we will learn how to use Promise and how to implement our own Promise. The explanation is very clear. It will be implemented step by step in the whole process, with detailed notes and principle explanation.
If you think this article is helpful to you, ❤️ Follow + like ❤️ Encourage the author, the official account, the first time to get the latest articles, reply to the group, pull you into the front end development group, reply to the data, get the massive front-end e-book and the front end learning video.
What is promise? What problems are mainly used to solve?
Promise is a solution for asynchronous programming, which is more reasonable and powerful than traditional solutions -- callback functions and events.
Promise features:
(1) The state of the object is not affected by the outside world. The Promise object represents an asynchronous operation with three states: pending, successful, and reject. Only the result of asynchronous operation can determine the current state, and no other operation can change this state. This is also the origin of the name Promise.
(2) Once the state changes, it will not change again. This result can be obtained at any time. There are only two possibilities for the state of Promise object to change: from pending to fully and from pending to rejected.
promise is mainly used to solve:
- Callback hell
- Concurrent request
- Asynchronous scheme optimization (but it is not asynchronous in itself. It will be executed immediately after new Promise())
promise basic usage
ES6 stipulates that Promise object is a constructor used to generate Promise instance
The following code creates a Promise instance
const promise = new Promise(function(resolve,reject){ //... if(/*Asynchronous operation succeeded*/){ resolve(value) }else{ //Asynchronous operation failed reject(error) } })
Promise constructor accepts a function as an argument. The two parameters of the function are resolve and reject They are two functions provided by the JavaScript engine and do not need to be deployed by themselves.
The resolve function is used to change the state of Promise object from "incomplete" to "successful" (i.e. from pending to resolve), call it when the asynchronous operation is successful, and pass the result of the asynchronous operation as a parameter; The reject function is used to change the state of the Promise object from "incomplete" to "failed" (i.e. from pending to rejected). It is called when the asynchronous operation fails, and the error reported by the asynchronous operation is passed as a parameter.
After the Promise instance is generated, you can use the then method to specify the callback function of resolved state and rejected state respectively, or use the catch method to specify the callback function of rejected state.
promise.then(res=>{ //success },error=>{ //error }).catch(err=>{})
The then method can accept two callback functions as parameters. The first callback function is called when the state of the Promise object changes to resolved, and the second callback function is called when the state of the Promise object changes to rejected. The second function is optional and does not have to be provided. Both functions receive the value from the Promise object as a parameter.
Ok, through the above description of the basic usage of promise, we know what should be included in a promise class:
- promise status: pending, fulfilled, rejected
- promise return value
- Executor: promise execution entry (that is, the function you passed in)
- resolve: change promise status to full
- reject: change the project status to rejected
- then: receive two callbacks, onfulfilled and onrejected. Execute after the promise state changes to completed or rejected
- catch: accept a callback and execute it after the promise state changes to rejected
Simple implementation of a promise
We know that a promise contains at least the above contents, so a simple promise is at least like this
class myPromise { static PENDING = 'pending' static FULFILLEd = 'fulfilled' static REJECTED = 'rejected' constructor(init){ this.state = myPromise.PENDING // promise status this.promiseRes = null // Return value of promise const resolve = result=>{ //... } const reject = result=>{ //... } try{ init(resolve,reject) // init is to initialize the actuator }catch(err){ reject(err) } } then(onFulfilled,onRejected){ //... } catch(onRejected){ //... } }
OK, after a general understanding, let's look at the implementation and function of each part one by one
Promise actuator
In fact, it is a callback function that we pass in during new Promise. The function itself is synchronous, that is to say, it will execute when new Promise, which is also the entry for us to operate promise.
class myPromise{ //... constructor(init){ try{ init(resolve,reject) // init is to initialize the actuator }catch(err){ reject(err) //This is mainly used to change the promise state to rejected when the init executor function makes an error } } //... }
This function takes two callback functions (resolve, reject) as parameters to change the state of Promise
resolve and reject methods
These two functions are passed to the actuator function as parameters to change the Promise state later
class myPromise { static PENDING = 'pending' static FULFILLEd = 'fulfilled' static REJECTED = 'rejected' constructor(init){ this.state = myPromise.PENDING // promise status this.promiseRes = null // promise return value this.resolveCallback = [] //Successful callback collection this.rejectCallback = [] //Failed callback collection const resolve = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.FULFILLEd //Change state this.promiseRes = result //Return value //Call successful callbacks successively this.resolveCallback.forEach(fn=>fn()) } } const reject = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.REJECTED //Change state this.promiseRes = result //Return value // Call failure callback successively this.rejectCallback.forEach(fn=>fn()) } } try{ init(resolve,reject) // Note that this points to }catch(err){ reject(err) } } }
Preliminary then method
class myPromise { static PENDING = 'pending' static FULFILLEd = 'fulfilled' static REJECTED = 'rejected' constructor(init){ this.state = myPromise.PENDING // promise status this.promiseRes = null // promise return value this.resolveCallback = [] //Successful callback collection this.rejectCallback = [] //Failed callback collection const resolve = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.FULFILLEd //Change state this.promiseRes = result //Return value //Call successful callbacks successively this.resolveCallback.forEach(fn=>fn()) } } const reject = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.REJECTED //Change state this.promiseRes = result //Return value // Call failure callback successively this.rejectCallback.forEach(fn=>fn()) } } try{ init(resolve,reject) // Note that this points to }catch(err){ reject(err) } } then(onFulfilled,onRejected){ if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') { onFulfilled(this.promiseRes) } if(this.state === myPromise.REJECTED && typeof onRejected === 'function') { onRejected(this.promiseRes) } } }
Here, our promise has been preliminarily formed. We can test it:
const res1 = new myPromise((res,rej)=>{ res('It worked~') rej('Failed~') }) res1.then((res)=>{ console.log(res) },err=>{ console.log(err) }) // As expected, this should only print out success ~
From the above figure, we can see whether it meets our expectations, and myPromise is very similar to the native Promise. Do you think there is no problem here? We just tested the execution of synchronous methods, but don't forget that Promise is mainly to solve asynchronous problems. Let's try again. Does the execution of asynchronous methods meet our expectations?
const res1 = new myPromise((res,rej)=>{ setTimeout(()=>res('It worked~'),1000) // rej('failed ~ ') }) res1.then((res)=>{ console.log(res) })
Here, we expect to print successfully after one second, but it doesn't print anything as we want. This is because the then method has been executed before setTimeout is executed, and then won't be executed again when the state becomes fully after 1s.
Therefore, we need to ensure that the callback function of the then method is executed when the promise state becomes fully or rejected. When the promise state is pending, we must first store the callback in the corresponding queue and execute it after the subsequent state changes
More complete then method
OK, here we modify our then method to ensure the correctness of asynchronous code execution (specifically, we can use mutationObserver to implement micro tasks, and here we will use setTimeout to simulate)
class myPromise { static PENDING = 'pending' static FULFILLEd = 'fulfilled' static REJECTED = 'rejected' constructor(init){ this.state = myPromise.PENDING // promise status this.promiseRes = null // promise return value this.resolveCallback = [] //Successful callback collection this.rejectCallback = [] //Failed callback collection const resolve = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.FULFILLEd //Change state this.promiseRes = result //Return value //Call successful callbacks successively this.resolveCallback.forEach(fn=>fn()) } } const reject = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.REJECTED //Change state this.promiseRes = result //Return value // Call failure callback successively this.rejectCallback.forEach(fn=>fn()) } } try{ init(resolve,reject) // Note that this points to }catch(err){ reject(err) } } then(onFulfilled,onRejected){ if(this.state === myPromise.FULFILLEd && typeof onFulfilled === 'function') { onFulfilled(this.promiseRes) } if(this.state === myPromise.REJECTED && typeof onRejected === 'function') { onRejected(this.promiseRes) } if(this.state === myPromise.PENDING){ if(onFulfilled && typeof onFulfilled === 'function'){ this.resolveCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ onFulfilled(this.promiseRes) },0) ) } if(onRejected && typeof onRejected === 'function'){ this.rejectCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ onRejected(this.promiseRes) },0) ) } } } }
Here, we can test the test case of the asynchronous function above and find that it can print correctly. OK, even if a more complete then method is implemented ~
Chain call of then
The then method returns a new Promise( ⚠️ Note: it is not the original Promise, so it can be called in a chain
Using chained then, you can specify a set of callback functions to be called in order. At this time, the previous callback function may return a Promise object (i.e. asynchronous operation). At this time, the latter callback function will wait for the state of the Promise object to change before being called.
then(onFulfilled,onRejected){ const {promiseRes,state} = this let promise = new myPromise((reso,reje)=>{ const resolveMyPromise = promiseRes => { try{ if(typeof onFulfilled !== 'function'){ // If the first callback of then is not a function, ignore it directly and return a new promise reso(promiseRes) }else{ // Get the execution result of the first callback const res = onFulfilled(promiseRes) // See whether the execution result is a promise if(res instanceof myPromise){ // Is a promise. Wait until its state changes, and then change the promise state returned by then res.then(reso,rej) }else{ // Not a promise, use it as the resolve of the new promise reso(res) } } }catch(err){ //Exception, directly set the new promise state to rejected reje(err) } } const rejectMyPromise = promiseRes => { try{ if(typeof onRejected !== 'function'){ // If the second callback of then is not a function, ignore it directly and return a new promise reje(promiseRes) }else{ // Get the execution result of the second callback const res = onRejected(promiseRes) // See whether the execution result is a promise if(res instanceof myPromise){ // Is a promise. Wait until its state changes, and then change the promise state returned by then res.then(reso,rej) }else{ // Not a promise, use it as the resolve of the new promise reje(res) } } }catch(err){ //Exception, directly set the new promise state to rejected reje(err) } } if(state === myPromise.FULFILLEd) { resolveMyPromise(promiseRes) } if(state === myPromise.REJECTED) { rejectMyPromise(promiseRes) } if(state === myPromise.PENDING){ if(onFulfilled && typeof onFulfilled === 'function'){ this.resolveCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ resolveMyPromise(this.promiseRes) },0) ) } if(onRejected && typeof onRejected === 'function'){ this.rejectCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ rejectMyPromise(this.promiseRes) },0) ) } } }) return promise }
catch method
We know that the second callback of then is actually the same as the catch method, so we can implement the catch method in this way
catch(onRejected) { return this.then(undefined,onRejected) }
Promise.resolve
Turning an object into a promise object can be divided into four cases according to the parameters
- Parameter is a Promise instance, which is returned directly
- The parameter is a thenable object. After the object is converted into a Promise object, the then method of the object is executed
- Without parameters, it also returns a new Promise object with the status of resolved
- The parameter is an original value and returns a new Promise object with the status of resolved
Manual implementation:
static resolve(v){ //1. The parameter is a Promise instance, which is returned directly if(v instanceof myPromise){ return v } //2. The parameter is a thenable object. After it is converted to Promise, execute the then method of the object if(typeof v === 'object' && typeof v.then === 'function'){ return new myPromise((res,rej)=>{ v.then(res,rej) }) } //3. Without parameters, return a promise in resolved status directly if(!v){ return new myPromise(res=>{ res() }) } //4. The parameter is an original value and returns a new Promise in the status of resolved return new myPromise(res=>{ res(v) }) }
Promise.reject
Returns a new Promise object with the status of rejected
static reject(v){ return new myPromise((res,rej)=>{ rej(v) }) }
Promise.all
This method is used to package multiple Promise instances into a new Promise instance. If there is an item that is not Promise, the item will succeed directly
Usage:
const p = Promise.all([p1,p2,p3])
The state of p is determined by p1, p2 and p3, which can be divided into two cases.
(1) Only when the states of p1, p2 and p3 become fully, the state of p will become fully. At this time, the return values of p1, p2 and p3 form an array and are passed to the callback function of p.
(2) As long as one of p1, p2 and p3 is rejected, the state of p becomes rejected. At this time, the return value of the first rejected instance will be passed to the callback function of p.
Ok, after understanding promise Let's do it again
Manual implementation:
static all (promises){ return new myPromise((res,rej)=>{ let count = 0 const result = []; function addFun(index,resf) { result[index]=resf // Use index instead of push here to ensure the order of return count++ if(count==promises.length) { res(result) } } [].forEach.call(promises,(promise,index)=>{ if(promise instanceof myPromise) { promise.then(success=>{ // count ++ // result.push(success) addFun(index,success) },err=>{ rej(err) }) }else{ addFun(index,promise) } }) }) }
Promise.race
Promise. The race () method also wraps multiple promise instances into a new promise instance.
Usage:
const p = Promise.race([p1, p2, p3]);
In the above code, as long as one instance of p1, p2 and p3 changes the state first, the state of p will change accordingly. The return value of the Promise instance that changed first is passed to the callback function of p.
Manual implementation:
static race(promises) { return new myPromise((res,rej)=>{ [].forEach.call(promises,promise=>{ if(promise instanceof myPromise){ promise.then(success=>{ res(success) },error=>{ rej(error) }) }else{ res(promise) } }) }) }
Complete code
class myPromise { static PENDING = 'pending' static FULFILLEd = 'fulfilled' static REJECTED = 'rejected' constructor(init){ this.state = myPromise.PENDING // promise status this.promiseRes = null // promise return value this.resolveCallback = [] //Successful callback collection this.rejectCallback = [] //Failed callback collection const resolve = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.FULFILLEd //Change state this.promiseRes = result //Return value //Call successful callbacks successively this.resolveCallback.forEach(fn=>fn()) } } const reject = result=>{ // Only when the status is pending can it be changed to ensure that once the status is changed, it will not change again if(this.state === myPromise.PENDING){ this.state = myPromise.REJECTED //Change state this.promiseRes = result //Return value // Call failure callback successively this.rejectCallback.forEach(fn=>fn()) } } try{ init(resolve,reject) // Note that this points to }catch(err){ reject(err) } } then(onFulfilled,onRejected){ const {promiseRes,state} = this let promise = new myPromise((reso,reje)=>{ const resolveMyPromise = promiseRes => { try{ if(typeof onFulfilled !== 'function'){ // If the first callback of then is not a function, ignore it directly and return a new promise reso(promiseRes) }else{ // Get the execution result of the first callback const res = onFulfilled(promiseRes) // See whether the execution result is a promise if(res instanceof myPromise){ // Is a promise. Wait until its state changes, and then change the promise state returned by then res.then(reso,rej) }else{ // Not a promise, use it as the resolve of the new promise reso(res) } } }catch(err){ //Exception, directly set the new promise state to rejected reje(err) } } const rejectMyPromise = promiseRes => { try{ if(typeof onRejected !== 'function'){ // If the second callback of then is not a function, ignore it directly and return a new promise reje(promiseRes) }else{ // Get the execution result of the second callback const res = onRejected(promiseRes) // See whether the execution result is a promise if(res instanceof myPromise){ // Is a promise. Wait until its state changes, and then change the promise state returned by then res.then(reso,rej) }else{ // Not a promise, use it as the resolve of the new promise reje(res) } } }catch(err){ //Exception, directly set the new promise state to rejected reje(err) } } if(state === myPromise.FULFILLEd) { resolveMyPromise(promiseRes) } if(state === myPromise.REJECTED) { rejectMyPromise(promiseRes) } if(state === myPromise.PENDING){ if(onFulfilled && typeof onFulfilled === 'function'){ this.resolveCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ resolveMyPromise(this.promiseRes) },0) ) } if(onRejected && typeof onRejected === 'function'){ this.rejectCallback.push(()=> // Here, we use setTimeout to simulate the micro task of realizing then setTimeout(()=>{ rejectMyPromise(this.promiseRes) },0) ) } } }) return promise } catch(onRejected) { return this.then(undefined,onRejected) } static all (promises){ return new myPromise((res,rej)=>{ let count = 0 const result = []; function addFun(index,resf) { result[index]=resf // Use index instead of push here to ensure the order of return count++ if(count==promises.length) { res(result) } } [].forEach.call(promises,(promise,index)=>{ if(promise instanceof myPromise) { promise.then(success=>{ addFun(index,success) },err=>{ rej(err) }) }else{ addFun(index,promise) } }) }) } static race(promises) { return new myPromise((res,rej)=>{ [].forEach.call(promises,promise=>{ if(promise instanceof myPromise){ promise.then(success=>{ res(success) },error=>{ rej(error) }) }else{ res(promise) } }) }) } static resolve(v){ //1. The parameter is a Promise instance, which is returned directly if(v instanceof myPromise){ return v } //After the object is converted to the parameter of Promise. 2 if(typeof v === 'object' && typeof v.then === 'function'){ return new myPromise((res,rej)=>{ v.then(res,rej) }) } //3. Without parameters, return a promise in resolved status directly if(!v){ return new myPromise(res=>{ res() }) } //4. The parameter is an original value and returns a new Promise in the status of resolved return new myPromise(res=>{ res(v) }) } static reject(v){ return new myPromise((res,rej)=>{ rej(v) }) } }
Recommended reading
Introduce reflow & repaint and how to optimize it?
These browser interview questions, see how many you can answer?
This time, I'll give you a thorough understanding of front-end local storage
Interviewer: tell me the difference between front-end routing and back-end routing
Prototype and prototype chain of JavaScript
Javascript deep scope and closure
this points to call,apply,bind
summary
OK, I went over the usage of Promise with you and realized Promise by myself. I think you will have a clearer understanding of Promise after reading this article.
I'm Nan Jiu. Thank you for your "praise and attention". I'll see you next time!