A prelude to

In the front end group of our company, there was an online problem caused by the wrong use of Promise. The starting point for this article is not only to understand promises, but also to follow features to write a Promise. So if you already know the basic features of Promise, if not, click Learn

The body of the

Be familiar with the Promise feature of ES6.

1. Feature Overview

  • Chain call.

  • There are three internal states.

The three internal states are pending, fullfiled, and Rejected. The initialization state is pending. Status changes can be converted from pending to Fullfiled or rejected. There is no other conversion method.

2. The background

Solve Ajax callback hell.

The essence of Ajax callback hell is to want uncontrollable asynchrony to become synchronous. Such as:

ajax1({... ,success(res1){ ... }}) ajax2({... ,params:{res1},success(res2){ // error no res1 ... }})Copy the code

When our ajax2 needs to use ajax1, we have to use nesting:

ajax1({... ,success(res1){ ajax2({... ,params:{res1},success(res2){ /// doing something }}) }})Copy the code

The biggest problem with this approach is that the code becomes difficult to maintain when there are many nesting layers.

So how is Promise written ajax modification:

p1 = function() {
  return  new Promise(function(resolve){ ajax1({... ,success(res1){ resolve(res1) }}) }) } p2 =function() {return new Promise(function(resolve){ ajax2({... ,params:{res1},success(res2){ /// doing something }}) }) }Copy the code

So the final rule of writing becomes

p1().then(res=>{
    return p2()
}).then(res2=>{
    // doing something
})
Copy the code

3 Specific features.

  • 1. To learn about the official Promise A+, click
  • 2. Then comes the usage, ruan Yifeng big ES6 Promise is particularly detailed. Click on the

Here we make the next few defining functions A convention according to Promise A+.

1. New Promise(). Then (), which is a pity and onRejected

Start writing

1.Promise is a function

So the first thing we’re going to do is pass a function

function Promise(executor) {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!'; }}Copy the code

2.Promise initial state.

Promise has three states, and you can use an enumeration of TS

enum pStatus  {
    pending = 'pending',
    fulled = 'fullfilled',
    rejected = 'rejected'
}
Copy the code

Then we need to define some properties.

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // Default state this.resovlecbs = []; // The resolve function of the callback comes from promise.prototype.then this.rejectcbs = []; // The reject callback comes from promise.prototype.then this.value; // Record the resolve value this.error; // Record reject value}Copy the code

We know that the function that Promise passes is going to execute directly on the main thread, so we need to execute it directly.

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // Default state this.resovlecbs = []; // The resolve function of the callback comes from promise.prototype.then this.rejectcbs = []; // The reject callback comes from promise.prototype.then this.value; // Record the resolve value this.error; // Record reject try {executor(resolve,reject); // The execution of the passed function. } catch (error) { reject(error); // Exceptions caught are executed directly reject. }}Copy the code

3. Start the chain structure of Promise to achieve asynchronous result synchronization and solve callback hell.

First of all, it should be noted that the chain structure works by constantly returning new promises, that is, the result of then

Promise.prototype.then = function() {
    return new Promise(function(resolve,reject){ xxx... })}Copy the code

The first thing we need to be clear about is, what are the Promise features?

1. The then values are the resolve and reject state callbacks, respectively.

2. Pass a value, passing the value of the previous THEN down.

3. In accordance with the first-come, first-come policy of the same layer, the upper layer must be executed first.

To see what the third feature means in detail, let’s look at an example:

var p1 = new Promise(function(resolve,reject){
    resolve('p1')}); var p2 = new Promise(function(resolve,reject){
    resolve('p2')}); p1.then(()=>{ console.log('p11')
}).then(()=>{
    console.log('p12')
})

p2.then(()=>{
    console.log('p21')
}).then(()=>{
    console.log('p22')})Copy the code

P11 => P21 => p12 => P22. Please refer to this article for reasons related to the features of macro and micro tasks, click Learn. I think you already know the third point.

So how do you do that?

Step:

1) The function passed to Promise, we will execute the function passed by THEN only after completion. That is

new Promise(function(resolve,reject){
    resolve('xxx') // (1) Execute only after executing thisthenThis is a pity function}). Then (function() {... XXX // (2) this is step 2})Copy the code

So then passes the onFulfilled function and onRejected function, which are both implemented in resolve. So then is just the function passed by then, stored in the resolvecbs and RejectCBS arrays inside the Promise main function.

I think one might ask why arrays?

Because you might write:

var p = new Promise(...) ; p.then(function() {... },...). ; p.then(function() {... },...). ; p.then(function() {... },...). ;Copy the code

All of this non-link stuff is actually storing onFullfilled inside the Promise, so you need an array.

Then there’s Promise inside resolve and reject. These two functions are passed in as arguments to functions passed in by the user. The essence is to iterate over the function items that execute Reslovecbs. And it changes the state, and it writes down the values that are passed in, and those values are passed to onFullfilled, and it’s up to onFullfilled to decide whether or not to pass them on. That is:

then(functionOnFullfilled (value){// value comes from the argument passed resolve.return value  // returnIt means to continue to the next onethenWhether you can get it. })Copy the code

Let’s add something like this:

function Promise() {
    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!'; } this.status = pStatus.pending; // Default state this.resovlecbs = []; // The resolve function of the callback comes from promise.prototype.then this.rejectcbs = []; // The reject callback comes from promise.prototype.then this.value; // Record the resolve value this.error; // Reject const resolve = (value:object)=>{this.value = value; / / record valuesthenThis.resovlecbs. forEach((item:Function)=>{item(value); }) this.status = pstatus. fulled; // Change the state to fullfilled} //... Reject Similarly try {executor(resolve,reject); } catch (error) { reject(error); }}Copy the code

Resolve executes a function stored in the resolveCbs array. These functions are pushed from then. But it is worth to note that in addition to the onFullfiled and onRejected functions passed by THEN, the two returned values are also passed. Therefore, the resolve of the next Promise will be implemented, because the first feature of resolve is to record values. So then looks like this.

Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) {

    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){ scope.resovlecbs.push((value)=>{ handlerRes(onFullfilled,value,resolve); }) scope.rejectcbs.push((error)=>{ handlerRes(onRejected,error,reject); })}); }export function handlerRes(handler,message,next){
    let res 
    if(isFunc(handler)){ res = handler(message); } next(res); // Execute the next function resolve}Copy the code

Resovlecbs and rejectcbs()/resolve/onFullfilled/rejectCBS ()/onFullfilled/onFullfilled/rejectCBS ()/onFullfilled/onFullfilled

And not just onRresolved will be implemented, but also resolve, the next Promise.

This has implemented sequential execution of the THEN chain.

In the case of the constructor New Promise(), the steps are to create an empty object and mount all the properties that the Promise performs internally to this object. That’s all the properties of this.

The effect picture of the transfer is as follows:

But there are two problems with this:

  • 1. The first-come-first-come policy of the same layer cannot be implemented. This effect is the formal event loop queue. So we can use microtasks or macro tasks. Here’s a simplified code structure to simulate using setTimeout. If you’re interested, check out the NPM library ASAP, click here

  • Resolvecbs (resolvecbs, resolvecbs, resolvecBs, resolvecBs, resolvecBs, resolvecBs, resolvecBs, resolvecBs) Such as:

var p1 = new Promise(function(resolve,reject){
    resolve('p1')}); p1.then(()=>{ console.log('p11')}).Copy the code

At this time, we will execute resolve first to complete the traversal of resolvecbs, and then collect resolvecbs through THEN. The functions collected after name are never executed. So we have to judge the state.

Hack writing:

Promise.prototype.then = function (onFullfilled:Function=noop,onRejected:Function=noop) {


    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){
        if(scope.status === pstatus.pending) {// Pending = scope. Resovlecbs.push ((value)=>{ handlerRes(onFullfilled,value,resolve); }) scope.rejectcbs.push((error)=>{ handlerRes(onRejected,error,reject); })}else if(the scope. The status = = = pStatus. Fulled) {/ / fullfilled, directly executed handlerRes (onFullfilled, scope. The value, resolve); }else{/ / rejectd direct execution handlerRes (onRejected, scope. The error, reject); }}); }Copy the code

A number of scenarios have been considered here, but do you think it’s over? Let’s see what Promise A+ says.

  • If ondepressing is not a function and promise1 executes successfully, promise2 must execute successfully and return the same value.
  • If onRejected is not a function and promise1 rejects execution, promise2 must reject execution and return the same value.

What do you mean? Let’s look at an example:

new Promise(function(resolve){
    resolve('test')
}).then().then(value=>{
    console.log(value)
})
Copy the code

Will the above situation be transmitted?

The answer is yes. That’s the standard for Promise A+. So we have to be compatible in THEN. Let’s modify the then function again.

Promise.prototype.then = function (onFullfilled:Function,onRejected:Function) {

    let scope = this;
    return new Promise(function(resolve = noop,reject = noop){

        const resolveHandler = function(value){
            if(isFunc(onFullfilled)) {
                handlerRes(onFullfilled,value,resolve);
            } else {
                resolve(value)
            }
        }
        const rejectHanlder = function(error) {
            if(isFunc(onRejected)){
                handlerRes(onRejected,error,resolve);
            } else {
                reject(error);
            }
        }

        try {
            if(scope.status === pStatus.pending) { scope.resovlecbs.push((value)=>{ resolveHandler(value) }) scope.rejectcbs.push((error)=>{ rejectHanlder(error); })}else if(scope.status===pStatus.fulled) {
                resolveHandler(scope.value);
            } else{ // rejectd rejectHanlder(scope.error); } } catch (error) { reject(error); }}); }function handlerRes(handler,message,next){
    let res 
    if(isFunc(handler)){ res = handler(message); } next(res); // Execute the next function resolve}Copy the code

Well, this is pretty much the end of the story, however, the issue of callback hell remains unresolved. Let’s see how we handle callback hell.

promise1.then(function() {returnConsole. log(value =>{console.log(value) // should get the result of promise2})Copy the code

The result of our onFullfilled function for then is currently in the handlerRes function, so we have to modify this function to accommodate a return of type Promise.

function handlerRes(handler,message,nextResolve,nextReject,Promise){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    
    if(res && res instanceof Promise) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                nextResolve(value)
            },err=>{
                nextReject(err)
            })
        }
    } else{ nextResolve(res); }}Copy the code

The processing of Promise has been added above, is that OK? That’s a problem if promise2 is also a deep promise. Such as promise2. Then (value = > {}); If value is an instance of a promise, then our handlerRes will still have problems.

So we need recursive processing, so let’s modify:

export function deepGet(res,Promise2,nextResolve,nextReject){
    if(res && res instanceof Promise2) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                deepGet(value,Promise2,nextResolve,nextReject)
            },err=>{
                nextReject(err)
            })
        }
    } else{ nextResolve(res); }}export function handlerRes(handler,message,nextResolve,nextReject,Promise2){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    deepGet(res,Promise2,nextResolve,nextReject)
}
Copy the code

A complete code example is as follows:

Promise2.prototype.then = function (onFullfilled:Function,onRejected:Function) {

    let scope = this;
    return new Promise2(function(resolve = noop,reject = noop){

        const resolveHandler = function(value){
            if(isFunc(onFullfilled)) {
                handlerRes(onFullfilled,value,resolve,reject,scope.constructor);
            } else {
                resolve(value)
            }
        }
        const rejectHanlder = function(error) {
            if(isFunc(onRejected)){
                handlerRes(onRejected,error,resolve,reject,scope.constructor);
            } else {
                reject(error);
            }
        }

        try {
            if(scope.status === pStatus.pending) { scope.resovlecbs.push((value)=>{ resolveHandler(value) }) scope.rejectcbs.push((error)=>{ rejectHanlder(error); })}else if(scope.status===pStatus.fulled) {
                resolveHandler(scope.value);
            } else{ // rejectd rejectHanlder(scope.error); } } catch (error) { reject(error); }}); }export function deepGet(res,Promise2,nextResolve,nextReject){
    if(res && res instanceof Promise2) {
        if(res.status===pStatus.pending){
            res.then(value=>{
                deepGet(value,Promise2,nextResolve,nextReject)
            },err=>{
                nextReject(err)
            })
        }
    } else{ nextResolve(res); }}export function handlerRes(handler,message,nextResolve,nextReject,Promise2){
    let res 
    if(isFunc(handler)){
        res = handler(message);
    }
    deepGet(res,Promise2,nextResolve,nextReject)
}
Copy the code

With the then function written, let’s go back to the Promise itself.

Promises are microtasks, and for convenience, macro task intervals are added to the Promise itself. Reject the same.

function Promise(executor:any) {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{
        this.value = value;
        setTimeout(()=>{
            this.resovlecbs.forEach((item:Function)=>{
                item(value);
            })
            this.status = pStatus.fulled;
        },0)
    }

    const reject = (error:Error)=>{
        this.error = error;

        setTimeout(()=>{ 
            this.status = pStatus.rejected;
            if(this.rejectcbs.length ===0){
                throw this.error;
            }  else{ this.rejectcbs.forEach((item:Function)=>{ item(error); / /}}}), 0)if(this.rejectcbs.length === 0 ) throw error; } try { executor(resolve,reject); } catch (error) { reject(error); }}Copy the code

However, this is still not the final version, as it does not resolve the problem of repeating resolvecbs multiple times. Therefore, the contents of resolve must be in the pending state before execution. For example, someone would do this:

new Promise(function(resolve){
    reslove('ddd')
    resolve('ttt')
}).then(value=>{
    console.log(value)
})
Copy the code

To print only one value, we must check resolve, which is only pending

function Promise(executor:any) {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{
        
        setTimeout(()=>{
            if(this.status=== pstatus.pending){// Avoid repeated execution. this.value = value; this.resovlecbs.forEach((item:Function)=>{ item(value); }) this.status = pStatus.fulled; },0)} const reject = (error: error)=>{setTimeout(()=>{ // why
            if(this.status=== pstatus.pending){// Added judgment to avoid repeating this. this.status = pStatus.rejected; // State changesif(this.rejectcbs.length ===0){
                    throw this.error;
                }  else{ this.rejectcbs.forEach((item:Function)=>{ item(error); })}},0) //if(this.rejectcbs.length === 0 ) throw error; } try { executor(resolve,reject); } catch (error) { reject(error); }}Copy the code

Almost perfect, but still A Promise A+ processing problem. When the value of resolve(value) is a Promise, as in:

let p1 = new Promise(function(resolve,reject){
    resolve('test')
})

new Promise(function(resolve,reject){resolve(p1)}).then(value=>{console.log(value) // Printtest. })Copy the code

For now, we’re going to return p1 directly to then’s onFullfilled. Keep working on it.

export default function Promise(executor:any)  {

    if( !isFunc(executor) ){
        throw 'Promise2 传递的参数不为functon!!!';
    }

    this.status = pStatus.pending;

    this.resovlecbs = [];
    this.rejectcbs = [];
    this.value;
    this.error;

    const resolve = (value:object)=>{

        if(value instanceof Promise) {//return value['then'](resolve, reject);
        }
        
        setTimeout(()=>{
            if(this.status===pStatus.pending){
                this.value = value;
                this.resovlecbs.forEach((item:Function)=>{
                    item(value);
                })
                this.status = pStatus.fulled;
            }
        },0)
    }

    const reject = (error:Error)=>{

        setTimeout(()=>{ // 
            if(this.status===pStatus.pending){
                this.error = error;
                this.status = pStatus.rejected;
                if(this.rejectcbs.length ===0){
                    throw this.error;
                }  else{ this.rejectcbs.forEach((item:Function)=>{ item(error); }) } } },0) } try { executor(resolve,reject); } catch (error) { reject(error); }}Copy the code

4. Catch and finally functions for promises.

The catch and finally functions are syntactic sugar and can be replaced by “then”. Think for a moment.

Here is the code:

Promise.prototype.catch = function(catchcb:Function) {
    returnthis.then(undefined, catchcb); / / is the essencethen
}


Promise.prototype.finally = function (callback) {
   returnThis. Then ((value)=>{//then
        callback();
        return value;
   },callback);
}
Copy the code

So let’s write it this way

p.then(onResolve,onReject).catch(onCatch).finally(onFinal);
Copy the code

It’s actually equal to

p.then(onResolve,onReject).then(undefined,onCatch).then(onFinal,onFinal);
Copy the code

5. Promise. Resolve.

Ruan yifeng gives four processing methods of this function.

  • 1. Deliver the Promise,
  • {then:function(){}}
  • 3. Pass non-thenbale values
  • 4. Nothing.

Resolve. Resolve is a Promise. The way I write it is

Promise.resolve = function(handler){
    if(  isObject(handler)  && 'constructor' inHandler &&handler. Constructor === this) {// Handler is a Promisereturn handler;
    } else if (isObject(handler) && isFunc(handler.then) ){ // thenable
        return new this(handler.then.bind(handler));
    }  else{/ / not thenablereturn new this(function(resolve){ resolve(handler); }}})Copy the code

You can see if:

In case 1, it returns exactly as it was.

Case 2 returns a Promise and passes the object’s then function as an argument into the Promise.

Resolve ();

6.Promise.reject.

Promise. Reject is not as troublesome as resolve. Pass the value directly out.

Promise.reject = function() {
    const args = Array.prototype.slice.call(arguments);
    return new this((resolve, reject) => reject(args.shift()));
}
Copy the code

7. Promise. All.

First, usage:

const promises = [2, 3, 5, 7, 11, 13].map(function (id) {
  return new Promise(function(resolve,reject){
    setTimeout(()=>{ resolve(id); },id); })}); Promise.all(promises).then(function (posts) {
  console.log(posts);
})
Copy the code

The first thing we pass in is an array. The second is all the arrays, and the second is that all of the array’s Promise will be executed before it enters the THEN function of All. So you need a tag record to keep track of all completed promises in real time.

And then the array that’s passed in May have promises or it may not have promises, so you need to hack. Distinguish whether there is a then function.

Promise.all = function(arr) {

    if( !isArray(arr) ){
        throw The 'all 'function does not pass an Array. ';
    }

    let args = Array.prototype.slice.call(arr);
    letresArr = Array.call(null,Array(arr.length)).map(()=>null); // Record all resultslethandlerNum = 0; // Process the tagreturn new this((resolve,reject)=>{
        for(leti = 0; i<args.length; i++){let ifunc = args[i];
            if(ifunc && isFunc(ifunc.then)) {// Whether the ifunc existsthenFunction. ifunc.then(value=>{ resArr[i] = value; handlerNum ++; // Tag addedif(handlerNum>=arr.length){// Complete resolve(resArr) // Complete array}},error=>{reject(error); }); }else{// non-thenable resArr[I] = ifunc; handlerNum ++; // Tag addedif(handlerNum>=arr.length){// Complete resolve(resArr) // complete array}}}}); }Copy the code

8. Promise. Race.

Just go to the code, and basically the fastest will be returned as a result

Promise2.race = function(arr) {
    if( !isArray(arr) ){
        throw 'race does not pass Array!! ';
    }

    let args = Array.prototype.slice.call(arr);
    let hasResolve = false;

    return new this((resolve,reject)=>{
        for(leti = 0; i<args.length; i++){let ifunc = args[i];
            if(ifunc && isFunc(ifunc.then) ) { ifunc.then(value=>{ ! hasResolve && resolve(value) },error=>{ ! hasResolve && reject(error); }); }else {
                hasResolve = true; ! hasResolve && resolve(ifunc) } } }) }Copy the code

The source address

Analyze your colleagues’ problems.

The source code is roughly as follows

let test = function() {
    return new Promise((resolve,reject)=>{
        reject(new Error('test'))
    })
}

Promise.resolve('new').then(res=>{
    test().then(res2=>{
        ...
    })
}).catch(err=>{
    // use err
    console.log(err)
})

Copy the code

The problem is that there is no err in the final catch.

Return onFullfilled () and onRejected (); return onRejected (); return onFullfilled (); return onRejected (); The problem is that a return is missing from the then.

Conclusion:

  • Resolve does three things.

    1) Record the value passed and prepare for delivery.

    2) Execute the onFullfilled function for then

    3) Change the state.

  • The execution of THEN varies according to the state.

  • Catch and finally are syntactic sugar for then. Finally does not mean that it will be executed.

  • Promise. Resolve can quickly create a Promise, which must return a Promise.

Want to express more clearly, so the content is too long…

If you have any questions, please ask them in the comments section and I will answer them asap.