This article has participated in the third phase of the “High production” track at the Digger Creators Camp. For more details, check out the third phase of the Digger Project | Creators Camp to “write” personal impact

Preface 📚

Promise is known to be one of the most popular interview questions that interviewers love to test. Before Monday, the knowledge was relatively superficial, and I saw the realization of promise on some surfaces, but only stayed at that level. But IN fact, I found the question easily knocked me over without a deeper understanding of how it works, and if the interviewer tried it a little differently. So ah, or honestly from beginning to end to study, so that when encountered, no matter how to test, ten thousand cases unchanged one, the principle of understanding, it is not so easy to be asked to pour.

Let’s start with this article ~🏷️

📰 one, JS synchronous mode and asynchronous mode

1. Single-threaded 💡

As we all know, JS design is based on single thread development, its original purpose is to only participate in the browser DOM node operation.

For a single thread, it means that only one task can be executed, and all tasks are queued in a queued mode.

So, the disadvantage of single threading is that while JS is running, the HTML is not rendered. Therefore, if a task is particularly time-consuming, it can easily lead to a page blocking situation.

In order to solve this problem, JS proposed synchronous mode and asynchronous mode solution.

2. Synchronization mode 💡

(1) Definition

Synchronous mode refers to the call stack in the function, in the order that the code is implemented, step by step.

(2) Legend

Next, we will use a piece of code to demonstrate the execution of the js function call stack. The specific code is as follows:

const func1 = () = > {
    func2();
    console.log(3);
}

const func2 = () = > {
    func3();
    console.log(4);
}

const func3 = () = > {
	console.log(5);
}

func1(); / / 5 4 3
Copy the code

Seeing here, I believe that many people have already conceived the specific execution order. Here’s a picture to show how it works:

For the stack data structure, it follows the last in, first out principle. Thus, when func1, func2, and func3 are placed on the call stack in turn, the contents of func3 are executed first, followed by func2, and then func1.

Therefore, for js synchronization mode, it is similar to the above function call stack.

3. Asynchronous mode 💡

(1) Examples

There is a wait time when the program encounters problems such as network requests or scheduled tasks.

Suppose a timer is set to 10s. If we put it in a synchronization task, the synchronization task will block code execution and we will wait for 10s before we see the results we want. The wait time for 1 timer might be fine, but what about 100 timers? We can’t wait 1000s to see the results we want, it’s hardly realistic.

So this time you need to asynchronous, through asynchronous to let the program not block code execution, flexible execution of the program.

(2) Definition

For synchronous mode, it can only be executed from top to bottom, line by line, and parsed line by line. That’s different from synchronous mode, where the output is what we want it to be, and there is no blocking like synchronous mode, so that the program can be controlled.

(3) How to implement js asynchronous

Compared to the synchronous pattern, the asynchronous pattern has a more complex structure. In addition to the call stack, it has two additional mechanisms, message queues and event loops. The event loop is also called event loop or event polling. Because JS is single-threaded, and asynchrony needs to be implemented based on callback, so event loop is the implementation principle of asynchronous callback.

The execution of JS in the program follows the following rules:

  • Front to back, line by line;
  • If an error occurs on one line, the following code is stopped.
  • The synchronous code is executed before the asynchronous code is executed.

Let’s look at an example:

console.log('Hi');

setTimeout(function cb1(){
    console.log('cb1'); //cb1 is the callback function
}, 5000);

console.log('Bye');

// Print order:
//Hi
//Bye
//cb1
Copy the code

As you can see from the above example code, JS executes the synchronous code first, so it prints Hi and Bye first, and then executes the asynchronous code, printing out CB1.

With this code as an example, let’s begin the process of event Loop.

(4) Event loop process

For the above code, the execution process is as follows:

From the figure above, you can analyze the path of this code. First console.log(‘Hi’) is the sync code that executes and prints the Hi directly. Then continue with the timer setTimeout. The timer is asynchronous code, so at this point the browser will hand it over to the Web APIs to handle this, so put it in the Web APIs, and then continue with console.log(‘Bye’), Console. log(‘Bye’) is the synchronization code, executed in the Call Stack, printing out the Bye.

At this point, the contents of the Call Stack are completely executed. When the contents of the Call Stack are empty, the browser will start looking for the next task in the Callback Queue. In this case, the message Queue will look for the next task in the Web API, following the first-in, first-out rule. When the Web API is empty, the tasks in the task queue will be passed to the Call Stack for execution, and cb1 will be printed out.

4. Callback function 💡

In the early days when we were dealing with asynchronous problems, we basically used the form of callback. The form is as follows:

// Get the first data
$.get(url1, (data1) = > {
    console.log(data1);
    
    // Get the second data
    $.get(url2, (data2) = > {
        console.log(data2);
        
        // Obtain the third data
        $.get(url3, (data3) = > {
            console.log(data3);
            
            // You can also get more data
        });
    });
});
Copy the code

As you can see from the above code, in the early days, when the data is called, it is a layer of layer, callback call callback, as if deep in the call hell, the data is also called very messy. So, because callbacks are so development-unfriendly, there are later promises.

Promise was first introduced by the CommonJS community, and then in 2015, ES6 wrote it into the language standard, unifying its use and providing promise objects natively. The emergence of promise, farewell to the era of callback hell, to solve the problem of callback hell.

Here’s a look at some of the magic uses Promise can have

📃 ii. Promise asynchronous solution

1. Three states of Promise 📂

(1) Promise the Promise

state meaning
pending Wait state, i.e., in the process, with no results yet. For example, a network request is being made, or the timer is running out of time.
fulfilled Satisfy the state that the event has been resolved and succeeded; When we pull backfulfilledIs in this state and will be called backthenFunction.
rejected The rejected state, where the event has been rejected, is a failure; When we pull backrejectIs in this state and will be called backcatchFunction.

(2) State interpretation

A Promise is an object that represents the result of an asynchronous task when it is completed. It has three states: pending, fulfilled, or rejected. The execution process is as follows:

If an asynchronous task is in the pending state, it means that the asynchronous function in the promise has not completed execution and is in the waiting state. Conversely, if the asynchronous function in the PROMISE executes, it will only go to two results:

  • fulfilledSaid,successful;
  • rejectedSaid,failure.

Once the final state changes from pending to fulfilled or rejected, the state is never reversible.

So, in summary, Promise objects have the following two characteristics:

  • promiseState of an objectFree from outside influencesOnce the state has been aroused, the function is given toweb APIIn the body of the functionIt is useless to perform any further operations;
  • There will be onlypendingfulfilledOr,pendingrejectedState, that is, either succeed or fail. Even if again topromiseObject to add a callback function, tooYou’re just going to get the same resultThat is, its states are allIt will not be changed again.

2. The changes and manifestations of the three states 📂

(1) Change of state

There are three main forms of promise: pending, fulfilled and rejected. When a promise in a pending state is returned, then and catch are not fired. The THEN callback is fired when a fulfilled state is returned. When a rejected state is returned, the catch callback is fired. How do they change between these states?

1

Let’s start with some code:

const p1 = new Promise((resolved, rejected) = >{});console.log('p1', p1); //pending
Copy the code

In the above code, the console prints something like this:

In this code, p1 has nothing to execute, so it is waiting for the state, so it is pending.

2) Demonstration 2

const p2 = new Promise((resolved, rejected) = > {
    setTimeout(() = > {
        resolved();
    });
});

console.log('p2', p2); //pending at the beginning of printing
setTimeout(() = > console.log('p2-setTimeout', p2)); //fulfilled
Copy the code

In the above code, the console prints something like this:

In this code, P2 initially prints the pending state because it is not executed into setTimeout. When setTimeout is executed later, the resolved function will be triggered and return a promise in the fulfilled state.

3) Demo 3

const p3 = new Promise((resolved, rejected) = > {
    setTimeout(() = > {
        rejected();
    });
});

console.log('p3', p3);
setTimeout(() = > console.log('p3-setTimeout', p3)); //rejected
Copy the code

In the above code, the console prints the following.

In this code, P3 initially prints pending because it is not executed into setTimeout. When a subsequent setTimeout is executed, similarly, it fires to the rejected function, which returns a promise in the rejected state.

After watching the changes of promise states, I believe you will have a certain understanding of when the three states of promise are triggered respectively. So let’s look at the performance of the Promise state.

(2) The performance of the state

  • pendingState, does not triggerthencatch
  • fulfilledState, which triggers subsequentthenCallback function.
  • rejectedState, which triggers subsequentcatchCallback function.

So let’s do a demonstration.

1

const p1 = Promise.resolve(100); //fulfilled
console.log('p1', p1);
p1.then(data= > {
    console.log('data', data);
}).catch(err= > {
    console.error('err', err);
});
Copy the code

In the above code, the console prints something like this:

In this code, p1 calls the resolved callback function in the promise. When p1 is in the fulfilled state, it will only trigger the.then callback, not the.catch, So we print out data 100.

2) Demonstration 2

const p2 = Promise.reject('404'); //rejected
console.log('p2', p2);
p2.then(data= > {
    console.log('data2', data);
}).catch(err= > {
    console.log('err2', err);
})
Copy the code

In the above code, the console prints something like this:

In this code, P2 calls the REJECT callback in the PROMISE. When executed, P1 is in the Reject state. In the REJECT state, the.catch callback is fired, not the.then callback, so err2 404 is printed.

3. Use case of Promise 📂

With a basic understanding of the three states, we use a case study to elaborate on the use of Promise. Now, what we want to implement is the ability to call local files asynchronously through the FS module. Output the contents of the file on the console if it exists. If the file does not exist, an exception will be thrown. The implementation code is as follows:

const fs = require('fs');

const readFile = (filename) = > {
    // Return a Promise instance for then invocation
    const promise = new Promise(function(resolve, reject){
        // Use readFile to read the file asynchronously, which is also what the promise function means
        // Note: the logic of this function is error first, that is, err first, then data
        fs.readFile(filename, (err, data) = > {
            // If the file fails to be read, reject is called and an exception is thrown
            if(err){
                reject(err);
            }else{
                // If the call was successful, call resolve and return the value of the successful callresolve(data); }}); });return promise;
}

// Test the code
// File has logic
const existedFile = readFile('./test.txt');
existedFile.then(
    (data) = > {
        The // buffer.from () method is used to create a new Buffer containing the specified string, array, or Buffer.
        // Buffer. From (data).toString() reads the contents of the file. File inside remember to write content!!
        console.log('content: ', Buffer.from(data).toString());
    },
    (error) = > {
        console.log(error); })// The file has no logic
const failFile = readFile('./fail.txt');
failFile.then(
    (data) = > {
        console.log(Buffer.from(data).toString());
    },
    (err) = > {
        console.log(err); });Copy the code

The final printing result of the console is as follows:

[Error: ENOENT: no such file or directory, open 'C:\\promise\\fail.txt'] {
  errno: -4058.code: 'ENOENT'.syscall: 'open'.path: 'C:\\promise\\fail.txt'
}
content: This is a test file!Copy the code

As you can see, when the./test.txt file is present, existedFile will call the subsequent.then callback, thus returning the result that the call was successful. Note that this is a test file! This is what the test file says.

At the same time, the./fail. TXT file does not exist, so failFile calls the subsequent.catch file and throws an exception.

Now that you’re familiar with the use of promises, let’s move on to the effects of then and catch on states in promises.

4. Effects of THEN and catch on states 📂

  • thenReturn to normalfulfilled, if there is an error, returnrejected
  • catchReturn to normalfulfilled, if there is an error, returnrejected

This is very depressing. This is very depressing. This is very depressing.

1

const p1 = Promise.resolve().then(() = > {
    return 100;
})
console.log('p1', p1); This will be fulfilled fulfilled. This will trigger a subsequent.then callback
p1.then(() = > {
    console.log('123');
});
Copy the code

In the above code, the console prints the following.

In this code, P1 calls the resolve callback function in the promise. When it is executed, P1 normally returns fulfilled without an error, so 123 is eventually printed.

2) Demonstration 2

const p2 = Promise.resolve().then(() = > {
    throw new Error('then error');
});
console.log('p2', p2); // The rejected state triggers the subsequent. Catch callback
p2.then(() = > {
    console.log('456');
}).catch(err= > {
    console.error('err404', err);
});
Copy the code

In the above code, the console prints the following.

P2 calls the resolve callback to the promise function and returns err404 Error because you have never met the promise function before. If you have never met the promise function before, you have never met the promise function before. Then error results.

This is very depressing. This is very depressing. This is very depressing.

1) Demo 1 (with special caution! !).

const p3 = Promise.reject('my error').catch(err= > {
    console.error(err);
});
console.log('p3', p3); This is very depressing. // This is very depressing. Triggers subsequent.then callbacks
p3.then(() = > {
    console.log(100);
});
Copy the code

In the above code, the console prints the following.

In this code, p3 calls the rejected callback and returns an Error during execution. This may seem a bit perverse, but promises will return to the fulfilled state if they return normally without throwing an exception, whether they are called resolved or Rejected. Therefore, the final state of P3 is fulfilled, and since it is fulfilled, the.then function can be called again.

2) Demonstration 2

const p4 = Promise.reject('my error').catch(err= > {
    throw new Error('catch err');
});
console.log('p4', p4); // The rejected state triggers the. Catch callback
p4.then(() = > {
    console.log(200);
}).catch(() = > {
    console.log('some err');
});
Copy the code

In the above code, the console prints the following.

Promise in this code, p4 still call the reject the callback function that is executed at this time, p4 in the process of execution, throws an Error, so, there is an Error, return to the rejected state, state of p4 is rejected, The subsequent.catch callback is then fired. So I’m going to print the result of some Err.

5. Parallel execution of Promise 📂

(1) Promise. All

The promise. all method is used to wrap multiple Promise instances into a new Promise instance. Such as:

var p = Promise.all([p1, p2, p3]);
Copy the code

The state of P is determined by P1, P2 and P3, which can be divided into two situations:

  • onlyp1p2p3Are in the state offulfilledAnd, ultimately,pWould change tofulfilled. At this timep1p2p3Form an array of the return values ofpCallback function.
  • As long asp1p2p3Any one of these three parameters isrejected, thenpThe state of theta is going to be thetarejected. Now the first one isrejectedThe return value of the instance ofpCallback function.

Here is an example of how promise.all can be used. The specific code is as follows:

// Generate an array of Promise objects
var promises = [4.8.16.74.25].map(function (id) {
    return getJSON('/post/' + id + ".json");
});

Promise.all(promises).then(fucntion (posts) {
	// ...
}).catch(function (reason) {
	// ...       
}}
Copy the code

Promises will be fulfilled someday, or one of them will become rejected. This is very depressing. This is very depressing.


A special case to note here is that the promise.all () catch method does not fire when the promise.all () is rejected if the promise.all () instance as an argument has its own catch method defined. This may be more abstract, let’s use an example to show, the concrete code is as follows:

const p1 = new Promise((resolve, reject) = > {
    resolve('hello');
}).then(result= > {
    return result;
}).catch(e= > {
    return e;
});

const p2 = new Promise((resolve, reject) = > {
    throw new Error('Error reported');
}).then(result= > {
    return result;
}).catch(e= > {
    return e;
});

Promise.all([p1, p2]).then(result= > {
    console.log(result);
}).catch(e= > {
    console.log(e);
})
Copy the code

In the above code, p1 will resolve and then call the subsequent.then callback. P2 rejects, so subsequent.catch callbacks are called. Note that p2 has its own catch method, and that method returns a new Promise instance, which P2 actually refers to.

As a result, this instance will become resolved after executing the catch method. Thus, in promise.all (), both instances in the argument are resolved, so the callback specified by the THEN method is called instead of the catch callback.

(2) the Promise. Race

The promise. race method also wraps multiple Promise instances into a new Promise instance. Such as:

var p = Promise.race([p1, p2, p3]);
Copy the code

We will also use this code for analysis. Unlike promise.all (), if one of the first instances in P1, P2, or p3 changes state, the state of ** p will change **, and the return value of the first promiser instance will be passed to the p callback function.

So why is it called Race? Race, as the name suggests, is a race. In the game, there is always one first place. If the first promise is in the first resolve state, then the first promise wins, so the value returned is the first value in the resolve state. No other race could escape the reality that they would not come first.

6. Two useful additional methods 📂

The Promise API in ES6 doesn’t provide many methods, but we can deploy some useful ones ourselves. Next, we’ll deploy two approaches that are not in ES6 but are useful.

(1) the done ()

Whether a Promise object’s callback chain ends in a THEN method or a catch method, if the last method throws an error, there is a chance that it will not be caught. Why is that? The reason is that errors within a Promise do not bubble up to the whole. Therefore, we provide a done method. The done method is always at the end of the callback chain, guaranteed to throw any possible errors. Let’s see how it works. The code is as follows:

asyncFunc()
	.then(f1)
	.catch(r1)
	.then(f2)
	.done();
Copy the code

In the meantime, the implementation code is relatively simple, so let’s take a look at that. The specific code is as follows:

Promise.prototype.done = function (onFulfilled, onRejected) {
    this.then(onFulfilled, onRejected) 
        .catch(function (reason) {
            // Throw a global error
            setTimeout(() = > {
                throw reason;
            }, 0); })}Copy the code

As you can see from the code above, the done method can be used just like the THEN method, providing callbacks for the fulfilled and rejected states, or it can be used without any arguments. But regardless, the done method catches any possible errors and throws them globally.

(2) finally ()

The finally method is used to specify an operation that will be performed regardless of the final state of the Promise object. The biggest difference from the done method is that it takes a normal callback function as an argument, which must be executed anyway.

Here’s an example. Let’s say we have a server, let’s let the server process requests using promises, and then use the finally method to shut down the server. The specific implementation code is as follows:

server.listen(0)
	.then(function () {
    // run test
}).finally(server.stop);
Copy the code

Again, the implementation code is relatively simple, so let’s take a look. The specific code is as follows:

Promise.prototype.finally = function (callback) {
    let p = this.constructor;
    return this.then(
        value= > p.resolve(callback()).then(() = > {
            return value;
        }),
        reason= > p.resolve(callback()).then(() = > {
            throwreason; })); };Copy the code

This will be fulfilled fulfilled or rejected. This will be fulfilled someday. This will be fulfilled someday.

📑 Iii. Implement Promise’s core functions

1. Basic core functions 🏷️

(1) Chatter

Let’s start by implementing the most basic of the promise core functions, promise.resolve() and promise.reject().

Note: With the exception of constructor, the underlying functionality is not implemented as a function bound to the prototype chain, and will be implemented using arrow functions.

(2) Analysis of Promise’s basic functions

Let’s take a look at the basic use of promise. The code is as follows:

/** * base use for 01_promise */
const promise = new Promise(function(resolve, reject) {
    if (success) {
        resolve(value);
    } else{ reject(error); }});Copy the code

According to the use mode, we can conclude that promise has the following characteristics:

  • promiseIs an object;
  • When we create a new onepromiseObject, we need to pass in a callback function;
  • This callback function needs to receive two more callbacksresolverejectAnd with thisTwo callback functions as parametersAnd then, when the call succeeds, useresolveCallback function, and when the call fails, userejectCallback function.
  • resolverejectBoth of these callbacks will be used to modifypromiseThe status of theresolvethependingChange the status tofulfilledAnd therejectIt will putpendingChange the status torejected. At the same time, it’s worth noting that,Once the state is determined, the state of all subsequent operations will not be changed again, that is, irreversible.

(3) Implementation of Promise basic functions

Let’s now implement the promise’s basic functionality, which consists of the following components:

  • implementationPromiseMonInfrastructure, which includes constructors and states;
  • implementationresolverejectFunction, here first implement the state frompendingfulfilledrejectedChanges between the remaining states have not yet been implemented.

The specific implementation code is as follows:

/** * 0_promise */

// Define the three constants pending, fulfilled and rejected
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
    // Define the state in a Promise. The default state is pending
    status = PENDING;

    // cb is the callback function passed to the promise
    constructor(cb) {
        // The cb callback is executed immediately to determine if the state should be changed
        cb(this.resolve, this.reject);
    }

    // The reason for using the arrow function is that the arrow function reduces the problem caused by this pointing, and binds it to the promise instance object
    // The resolve callback function
    resolve = () = > {
        // This can be changed only when the state is pending
        if(this.status ! = PENDING) {return;
        }else{
            this.status = FULFILLED; }};// Reject callback
    reject = () = >{this can only be modified if the state is pendingif(this.status ! = PENDING) {return;
        }else{
            this.status = REJECTED; }}}// Call resolve and reject to verify that the state is not reversible once it is determined
const promise1 = new PromiseMon((resolve, reject) = > {
    resolve('resolved');
    reject('rejected');
});

const promise2 = new PromiseMon((resolve, reject) = > {
    reject('rejected');
    resolve('resolved');
});

console.log(promise1.status); // fulfilled
console.log(promise2.status); // rejected
Copy the code

(4) Analysis of thenable function

For now, let’s continue to use this function to implement thenable.

As you know, after a promise calls the resolve and reject methods, it’s time to fire the subsequent.then or.catch methods. Without these two methods, the data returned by the promise would be useless, so why return the data?

Let’s look at the basic use of then for now. The specific code is as follows:

const fs = require('fs');

const readFile = (filename) = > {
    const promise = new Promise(function(resolve, reject){
        fs.readFile(filename, (err, data) = > {
            if(err){
                reject(err);
            }else{ resolve(data); }}); });return promise;
}

const existedFile = readFile('./test.txt');
existedFile.then(
    (data) = > {
        console.log('content: ', Buffer.from(data).toString());
    },
    (error) = > {
        console.log(error); })Copy the code

From the above code, let’s analyze several characteristics of the THEN function:

  • thenfunctionReceive two parameters, the first parameter is inThe asynchronous operation succeedsThe second one is inWhen the operation failsThe call.
  • thenThe function needs to be able to analyzepromiseWe’re donepromiseBefore deciding to callA success or failure callback function.
  • thenMethods are defined inA prototype objectOn:Promise.prototype.then()
  • thencallSuccessful callback functionWill receive aSuccessful dataSimilarly, when it calls a callback function that fails, it receives a reason for the failure before it passes it as an argument.
  • thenI’m going to receive one firstSuccess status dataThen this data is used toAs a parameter, this parameter is suppliedA callback function that handles the success stateMake a call; Similarly, when dealing with failure states,thenI’m going to receive one firstFailure status dataAnd then this data is usedAs a parameter, this parameter is suppliedA callback function that handles the failed stateIs called. Said so round, to sum up:Receive current status dataData as parameterTo be called by the callback function.

(5) Implementation of thenable function

Let’s now implement the basic functionality of Thenable, which has the following components:

  • Will be implemented on a prototypethenMethods;
  • Modify the originalresolveFunction, implement rightSuccess status dataBind;
  • Modify the originalrejectFunction, implement rightCause of the failed stateTo bind.

The specific implementation code is as follows:

/** * 03_thenable */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
    status = PENDING;
    // Define the value for success and failure. Default is undefined
    value = undefined;
    reason = undefined;

    constructor(cb) {
        // The cb callback is executed immediately to determine if the state should be changed
        cb(this.resolve, this.reject);
    }

    // Modify the parameter so that resolve receives the value passed after success
    resolve = (value) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = FULFILLED;
            // Assign the successful value to value
            this.value = value; }};// Modify the parameter to allow reject to accept the cause of a failure
    reject = (reason) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = REJECTED;
            // Assign the reason for failure to reason
            this.reason = reason; }}This is fulfilled, and this is fulfilled when successCB is fulfilled. This is fulfilled when failCB is fulfilled when failCB is fulfilled 
    then(successCB, failCB) {
        if(this.status === FULFILLED) {
            successCB(this.value);
        }else if(this.status === REJECTED) {
            failCB(this.reason); }}}// Test case
// Successful status test
const successPromise = new PromiseMon((resolve, reject) = > {
    resolve('successData');
    reject('failData');
});

console.log('Success status:', successPromise.status); // Success status: successData

successPromise.then(
    (value) = > {
        console.log('success:', value); // success:successData
    },
    (reason) = > {
        console.log('error:', reason); // No output})// Failed the status test
const failPromise = new PromiseMon((resolve, reject) = > {
    reject('failData');
    resolve('successData');
});

console.log('Failure status:', failPromise.status); // Failure status: failData

failPromise.then(
    (value) = > {
        console.log('success:', value); // No output
    },
    (reason) = > {
        console.log('error:', reason); // error:failData})Copy the code

At this point, we have implemented a very basic Promise that is executed synchronously. Let’s add asynchronous logic to this synchronous Promise.

2. Add the asynchronous logic function 🏷️

(1) Add analysis of asynchronous logic functions in THEN

In general, most of the functions we call in promises are asynchronous functions such as setTimeout, setInterval, and so on. So, we’re going to add asynchronous functionality to then to operate on asynchronous logic.

In the then method above, you go to if… Else if… This logic will judge only if the state is fulfilled and rejected, but not if the state is pending.

Therefore, when the state is pending, it means that the asynchronous functions in the PromiseMon have not finished executing. In this case, we need to save the succesCB and failCB callback functions in a variable and then call them after the asynchronous content ends.

(2) Add the implementation of asynchronous logic function in then

Based on the above analysis, let’s implement this asynchronous function. The specific code is as follows:

/** * 04_ Add asynchronous logic function implementation */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
    status = PENDING;
    value = undefined;
    reason = undefined;
    // Define two variables to hold the success and failure callbacks
    successCB = undefined;
    failCB = undefined;

    constructor(cb) {
        cb(this.resolve, this.reject);
    }

    resolve = (value) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = FULFILLED;
            this.value = value;
            /** * expression a && expression b: * Evaluates the result of expression a, * if true, executes expression b and returns the result of b; If false, return the result of a. * /
            SuccessCB is true if successCB has a callback for successCB. * It continues to determine if the status of the new value passed is success, * if so, it passes the new value to this. SuccessCB callback, * if not, Returns the result */ of this. Success previously stored 
            this.successCB && this.successCB(this.value); }}; reject =(reason) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = REJECTED;
            this.reason = reason;
            // Store the callback function that failed to be called
            this.failCB && this.failCB(this.reason); }}This is fulfilled, and this is fulfilled when successCB is fulfilled. This is fulfilled when failCB is fulfilled when failCB is fulfilled 
    then(successCB, failCB) {
        if(this.status === FULFILLED) {
            successCB(this.value);
        }else if(this.status === REJECTED) {
            failCB(this.reason);
        }
        // When a function has not finished executing, it can only wait
        else{ 
            // Store the values of the two callback functions
            this.successCB = successCB;
            this.failCB = failCB; }}}// Test case
// Test asynchronous success
const asyncPromise1 = new PromiseMon((resolve, reject) = > {
    setTimeout(() = > {
        resolve('asyncSuccessData');
    }, 2000);
});

asyncPromise1.then(
    (value) = > {
        console.log('Asynchronous success status:', value);
    },
    (reason) = > {
        console.log('Asynchronous failure status :', reason); });// Test the asynchronous failure status
const asyncPromise2 = new PromiseMon((resolve, reject) = > {
    setTimeout(() = > {
        reject('asyncErrorData');
    }, 1000);
});

asyncPromise2.then(
    (value) = > {
        console.log('Asynchronous success status:', value);
    },
    (reason) = > {
        console.log('Asynchronous failure status :', reason); });/** * Print result: * Asynchronous failure status: asyncErrorData * Asynchronous success status: asyncSuccessData */
Copy the code

At this point, the asynchronous function we also realize! But with the “THEN” above, we only implement one call to the “THEN” method. Next, we implement multiple calls to the “THEN” method.

3. Implement multiple calls to the THEN method 🏷️

(1) Function analysis of multiple calls to the THEN method

Calling the THEN method multiple times breaks down into two cases:

  • A synchronous invocationthenMethods. A synchronous invocationthenMethod is relatively simple, just call it directlysuccessCBfailCBThe callback function is fine.
  • The asynchronous callthenMethods. Previous propertiessuccessCBfailCBThe two callback functions are stored as objects, so we need to optimize our storage first. Once the optimization is complete, all the callback functions are stored together and called in turn after execution.

(2) The implementation of the function of calling the THEN method multiple times

Based on the above analysis, let’s implement the ability to call the THEN method multiple times. The specific code is as follows:

/** * 05_ Implement the then method function */

 const PENDING = 'pending';
 const FULFILLED = 'fulfilled';
 const REJECTED = 'rejected';
 
 class PromiseMon {
    status = PENDING;
    value = undefined;
    reason = undefined;
    // Define two array variables to hold all callbacks for success and failure, respectively
    successCB = [];
    failCB = [];

    constructor(cb) {
        cb(this.resolve, this.reject);
    }
 
    resolve = (value) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = FULFILLED;
            this.value = value;
            // Use the shift() method to pop up and return the first element
            while(this.successCB.length){
                this.successCB.shift()(this.value); }}}; reject =(reason) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = REJECTED;
            this.reason = reason;
            // Use the shift() method to pop up and return the first element
            while(this.failCB.length){
                this.failCB.shift()(this.reason); }}}then(successCB, failCB) {
        if(this.status === FULFILLED) {
            successCB(this.value);
        }else if(this.status === REJECTED) {
            failCB(this.reason);
        }else{
            // Use the push method to store the value of the callback function in the array
            this.successCB.push(successCB);
            this.failCB.push(failCB); }}}// Test case
const multiplePromise1 = new PromiseMon((resolve, reject) = > {
    setTimeout(() = > {
        resolve('multiSuccessData');
    }, 2000);
});
multiplePromise1.then((value) = > {
    console.log('First call successful:', value); // First call successful: multiSuccessData
});
multiplePromise1.then((value) = > {
    console.log('Second call successful:', value); // The second call succeeded: multiSuccessData
});

/** * Print the result: * First call successful: multiSuccessData * Second call successful: multiSuccessData */
Copy the code

That completes the implementation of calling the THEN method on this. Now, let’s move on to implementing chained calls to the THEN method.

4. Implement a chain call to the THEN method 🏷️

(1) Function analysis of the chain call of the THEN method

Let’s start with a functional analysis of chained calls to the THEN method, as follows:

  • Complete the nesting of chained calls without considering other functions;
  • To implement chained callsMajor premiseYes, every one of themthenAll functions must return onePromiseObject, otherwise it cannot be used coherentlythenFunction.
  • So, first of all, we need to look atthenCreate a new one in the functionPromiseObject, and then, in the newpromiseObject to handle internal useresolverejectThe returned value, finallythenThe function returnspromiseObject.

(2) Then method chain call function implementation

Based on the above functional analysis, let’s implement the chain call function of then. The specific code is as follows:

/** * 06_then */

 const PENDING = 'pending';
 const FULFILLED = 'fulfilled';
 const REJECTED = 'rejected';
 
 class PromiseMon {
    status = PENDING;
    value = undefined;
    reason = undefined;
    // Define two array variables to hold all callbacks for success and failure, respectively
    successCB = [];
    failCB = [];

    constructor(cb) {
        cb(this.resolve, this.reject);
    }
 
    resolve = (value) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = FULFILLED;
            this.value = value;
            while(this.successCB.length){
                this.successCB.shift()(this.value); }}}; reject =(reason) = > {
        if(this.status ! = PENDING) {return;
        }else{
            this.status = REJECTED;
            this.reason = reason;
            while(this.failCB.length){
                this.failCB.shift()(this.reason); }}}then(successCB, failCB) {
        // 
        /** * Create a promise object for the next then. * if the promise object is successfully executed, call resolve; * ② Reject is called when the PROMISE object fails; * ③ The Promise object has not yet been executed, pushing the callback into the prepared array. * /
        const thenablePromise = new PromiseMon((resolve, reject) = > {
            if(this.status === FULFILLED) {
                const thenableValue = successCB(this.value);
                // Check whether the returned value is a PROMISE object
                resolvePromise(thenableValue, resolve, reject);
            }else if(this.status === REJECTED) {
                const thenableReason = failCB(this.reason);
                // Check whether the returned value is a PROMISE object
                resolvePromise(thenableReason, resolve, reject);
            }else{
                // Use the arrow function to store the value of the callback function into an array
                this.successCB.push(() = > {
                    const thenableValue = successCB(this.value);
                    resolvePromise(thenableValue, resolve, reject);
                });
                this.failCB.push(() = > {
                    const thenableReason = failCB(this.reason); resolvePromise(thenableReason, resolve, reject); }); }});returnthenablePromise; }}/** * Determine if the thenablePromise is a Promise object, * if so, call the then function. If not, the value is returned. * /
const resolvePromise = (thenablePromise, resolve, reject) = > {
    // Check whether it is a Promise object
    if(thenablePromise instanceof PromiseMon) {
        thenablePromise.then(resolve, reject);
    } else {
        // If it is not a promise object, return the value directlyresolve(thenablePromise); }}// Test case
// Test the success status of the chain call
const thenablePromise = new PromiseMon((resolve, reject) = > {
    resolve('thenableSuccessData');
});

const otherPromise = () = > {
    return new PromiseMon((resolve, reject) = > {
        setTimeout(() = > {
            resolve('otherPromise');
        }, 2000);
    });
}

const anotherPromise = () = > {
    return new PromiseMon((resolve, reject) = > {
        setTimeout(() = > {
            resolve('anotherPromise');
        });
    });
}

thenablePromise
    .then((value) = > {
        console.log('First chaining call successful:', value, new Date()); // Successful first call: thenableSuccessData
        // The result of return is to be used for the next THEN
        return otherPromise();
    })
    .then((value) = > {
        console.log('Second chaining call successful:', value, new Date()); // The second call succeeded: otherPromise
        return anotherPromise();
    })
    .then((value) = > {
        console.log('Third successful chaining call:', value, new Date()); // The third call succeeds: anotherPromise
    })

ThenableSuccessData 2021-08-04T11:13:25.868z thenableSuccessData 2021-08-04T11:13:25.868z OtherPromise 2021-08-04T11:13:25.877z * Third chain call successful: anotherPromise 2021-08-04T11:13:25.878z */
Copy the code

At this point, we complete the chained call to promise.

(3) Self-detection of chained calls

Sometimes, when we call a Promise, we get into a situation where we call ourselves. That is, infinite nesting of loops and infinite self-calls. For example:

const promise1 = new Promise((resolve, reject) = > {
  resolve('success');
});

// Nested infinite loop, infinite self call
// The console throws an exception when run
const promise2 = promise1.then((val) = > {
  return promise2;
});

// Print the result:
// TypeError: Chaining cycle detected for promise #
      
Copy the code

So, what we need to do now is add a new parameter to the resolvePromise function. This parameter is the currently created Promise object. We then determine whether the two Promise objects are equal, and if they are, we throw an exception. According to this logic, let’s modify the function code of the above chaining call to achieve the closed loop that prohibits self-call. The specific code is as follows:

/** * 07_ Self-test for chained calls */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
  status = PENDING;
  value = undefined;
  reason = undefined;
  successCB = [];
  failCB = [];

  // The constructor code is omitted
  // Omit the resolve code
  // Omit the reject code

  then(successCB, failCB) {
      const thenablePromise = new PromiseMon((resolve, reject) = > {
        /** * Use setTimeout as an asynchronous function, * so that the contents of the setTimeout function are not implemented until after the synchronous function has finished executing, * so that when the resolvePromise is executed, the thenablePromise is instantiated. * makes the resolvePromise call thenablePromise */ smoothly
          if(this.status === FULFILLED) {
            setTimeout(() = > {
              const thenableValue = successCB(this.value);
              resolvePromise(thenablePromise, thenableValue, resolve, reject);
            }, 0);
          }else if(this.status === REJECTED) {
            setTimeout(() = > {
              const thenableReason = failCB(this.reason);
              resolvePromise(thenablePromise, thenableReason, resolve, reject);
            }, 0);
          }else {
            this.successCB.push(() = > {
              setTimeout(() = > {
                const thenableValue = successCB(this.value);
                resolvePromise(thenablePromise, thenableValue, resolve, reject);
              }, 0);
            });
            this.failCB.push(() = > {
              setTimeout(() = > {
                const thenableReason = failCB(this.reason);
                resolvePromise(thenablePromise, thenableReason, resolve, reject);
              }, 0); }); }});return thenablePromise;
  }

const resolvePromise = (createPromise, thenablePromise, resolve, reject) = > {
  if(createPromise === thenablePromise) {
    return reject(new TypeError('Chaning cycle detected'))}else if(thenablePromise instanceof PromiseMon) {
      thenablePromise.then(resolve, reject);
  } else{ resolve(thenablePromise); }}// Test case
// The test calls itself
const thenablePromise = new PromiseMon((resolve, reject) = > {
  resolve('chainningData');
});

const chainingPromise = thenablePromise.then((value) = > {
  console.log('Data call successful', value, new Date());
  return chainingPromise;
})
// An error is reported, and a circular call occurs
chainingPromise
  .then(
    (value) = > {
      console.log('Operation performed successfully', value, new Date());
    },
    (reason) = > {
      console.log('Failed to perform operation', reason, new Date()); });/* Print result: data call succeeded chainningData 2021-08-04T11:28:39.984z Operation failed TypeError: Loop call Chaning cycle detected */
Copy the code

At this point, we are done self-testing the chained call.

5. Error handling for promise 🏷️

(1) Error handling scenario

So far, our THEN method for Pormise has basically worked out pretty well. But there’s another important issue that we can easily overlook, which is error handling. Now, let’s examine a common scenario where an exception might occur:

  • Callback function in the constructorcb, need totry/catchProcessing;
  • thenError handling in theSynchronous and asynchronous functionsfortry/catchTo deal with.

(2) Implementation of error handling function

Based on the scenario analysis above, let’s implement promise’s error handling. The specific code is as follows:

/** * error handling for 08_promise */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
  status = PENDING;
  value = undefined;
  reason = undefined;
  successCB = [];
  failCB = [];

  constructor(cb) {
    try {
        cb(this.resolve, this.reject);
    } catch (err) {
        this.reject('err in cb'); }}// Omit the resolve code
  // Omit the reject code

  then(successCB, failCB) {
      const thenablePromise = new PromiseMon((resolve, reject) = > {
        // Add a try/catch to the asynchronous function setTimeout
          if(this.status === FULFILLED) {
            setTimeout(() = > {
              try {
                const thenableValue = successCB(this.value);
                resolvePromise(thenablePromise, thenableValue, resolve, reject);
              } catch(err) { reject(err); }},0);
          }else if(this.status === REJECTED) {
            setTimeout(() = > {
              try {
                const thenableReason = failCB(this.reason);
                resolvePromise(thenablePromise, thenableReason, resolve, reject);
              } catch(err) { reject(err); }},0);
          }else {
              // ResolvePromises are added to both successCB and failCB for processing
              this.successCB.push(() = > {
                  setTimeout(() = > {
                      try {
                        const thenableValue = successCB(this.value);
                        resolvePromise(thenablePromise, thenableValue, resolve, reject);
                      } catch(err) { reject(err); }},0);
              });
              this.failCB.push(() = > {
                  setTimeout(() = > {
                    try {
                        const thenableReason = failCB(this.reason);
                        resolvePromise(thenablePromise, thenableReason, resolve, reject);
                    } catch(err) { reject(err); }},0); }); }});return thenablePromise;
  }

  // Omit the resolvePromise code

// Test case
const promise = new PromiseMon((resolve, reject) = > {
    setTimeout(() = > {
        resolve('successData');
    });
});

promise
    .then(
        (value) = > {
            console.log(value); // (1) Print 'successData'
            throw new Error('error'); // (2) Throw an exception
        },
        (reason) = > {
            console.log(reason);
            return 'fail in then';
        }
    )
    .then(
        (value) = > {
            console.log(value);
        },
        (reason) = > {
            console.log(reason);
            return 'callback fail'; // Throw an exception and return 'callback fail' to the following then aspect call
        }
    )
    .then(
        (value) = > {
            console.log(value);
        },
        (reason) = > {
            console.log(reason); // (3) callback fail, so print 'callback fail'});/* Print the result: successData Error: Error callback fail */
Copy the code

As you can see, a try/catch approach is used to handle the error encountered, and eventually an exception is thrown and the value of reason is returned.

6. The parameter to implement the THEN method is optional 🏷️

(1) This parameter is optional

As you can see above, when we call the then method, we always need to pass arguments, which doesn’t seem particularly friendly. Therefore, let’s implement this function so that the arguments of the THEN method can be passed or not passed. The simple idea is to determine whether the argument was passed in the then function, and if not, return the original value. Something like this:

promise
  .then((val) = > val) // Using the arrow function in this way indicates that the value is returned directly
  .then((val) = > val) 
  .then((val) = > val) 
  .then((val) = > {
    console.log(val); / / 200
  });
Copy the code

(2) Parameter optional function realization

Based on the above is the implementation idea, let’s implement this function. The specific code is as follows:

Let’s change the then function:

then(successCB, failCB) {
    successCB = successCB ? successCB : (value) = > value;
    failCB = failCB ? failCB : (reason) = > { 
        throw reason;
    };
}
Copy the code

To test with two sets of test cases:

// Test case
// The use case in the successful state
 const successpromise = new PromiseMon((resolve, reject) = > {
     resolve(100);
 });
 
 successpromise
    .then()
    .then()
    .then()
    .then((val) = > {
        console.log(val); / / 100
    });

// Use cases in the failed state
const failPromise = new PromiseMon((resolve, reject) = > {
    reject(200);
});

failPromise
    .then()
    .then()
    .then()
    .then(
        (val) = > {},
        (reason) = > {
            console.log(reason); / / 200});/** * Print result: * 100 * 200 */
Copy the code

As you can see, both the Resolve and Reject states complete the implementation of optional parameters.

7. Realize the Promise. All 🏷 ️

(1) Promise. All function analysis

The promise.all and promise.race methods were discussed above when we talked about the Promise asynchronous solution. Now, let’s comb through the implementation ideas:

  • Promise.allIs aA static method, it receives onePromise arrayAs a parameter.
  • Promise.allIs that it canAccording to the orderTo get all the asynchronous functions that are called.
  • jsThe keywordstaticI’m going to put the correspondingA variable or function.Bound to theClass classInstead of being bound to **prototypeOn the prototype.

(2) Promise.all function implementation

According to the implementation logic, promise.all is implemented. The specific code is as follows:

/** * 10_promise.all */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
    status = PENDING;
    value = undefined;
    reason = undefined;
    successCB = [];
    failCB = [];

    // Omit the constructor, resolve, reject, and THEN methods

    static all(arr){
        const results = [];
        let index = 0;

        return new PromiseMon((resolve, reject) = > {
            // Add data logic to add the specified data to the corresponding position in the array
            const addData = (idx, val) = > {
                results[idx] = val;
                index++;
                // The purpose of this step is to wait for the asynchronous operation to complete
                if(index === arr.length) { resolve(results); }}// Loop through the array to get all the data
            arr.forEach((cur, index) = > {
                // If the value passed in is a Promise object
                if(cur instanceof PromiseMon){
                    cur.then(
                        (value) = > addData(index, value),
                        (reason) = > reject(reason)
                    )
                }
                // If a promise object is passed in with a normal value instead of a promise object
                else{ addData(index, cur); }}); }); }}// Omit the resolvePromise code

// Test case
const promise = () = > {
    return new PromiseMon((resolve, reject) = > {
        resolve(100);
    });
}

const promise2 = () = > {
    return new PromiseMon((resolve, reject) = > {
        setTimeout(() = > {
            resolve(200);
        }, 1000);
    });
}

// Because the static keyword is used, it can be called directly on the class
PromiseMon.all(['a'.'b', promise(), promise2(), 'c']).then((res) = > {
    console.log(res); // [ 'a', 'b', 100, 200, 'c' ]
})

Copy the code

As you can see, even though PROMISE2 is an asynchronous function, it will eventually be displayed in arrays and printed sequentially. At this point, it means promise.all has been successfully implemented!

At the same time, promise.race is implemented according to this pattern, which will not be explained here

8. Realize the Promise. Resolve 🏷 ️

(1) Promise. Resolve function analysis

Let’s look at the implementation of promise.resolve:

  • If the argument is apromiseInstance, sopromise.resolve()The instance will be returned unchanged, without any modifications.
  • Parameter is notpromiseInstance, or is not an object at allPromise.resolve()Method returns a newpromiseObject whose state isfulfilled
  • Returns one without any argumentsfulfilledThe state of thepromiseObject.

(2) Promise. Resolve function implementation

Implement promise.resolve based on the above functional analysis. The specific code is as follows:

/** * 11_promise.resolve */

const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

class PromiseMon {
    status = PENDING;
    value = undefined;
    reason = undefined;
    successCB = [];
    failCB = [];

    // Omit the constructor, resolve, reject, and THEN methods

    static resolve(value){
        // A promise object is passed in and returned intact
        if(value instanceof PromiseMon) {
            return value;
        } 
        // Return a promise as an argument
        else {
            return new PromiseMon((resolve, reject) = >{ resolve(value); })}}}// Omit the resolvePromise code

// Test case
const promise = () = > {
    return new PromiseMon((resolve, reject) = > {
        resolve(100);
    });
}

// 1. If the argument is a Promise instance, return the instance as it is
PromiseMon.resolve(promise).then((res) = > {
    console.log(res); / / 100
})

/** * 2. If the parameter is not an object with the then method, or is not an object at all, the promise.resolve () method returns a new Promise object with the state fulfilled */
PromiseMon.resolve(200).then((res) = > {
    console.log(res); / / 200
})

/** * 3. Return a resolved PROMISE object with no arguments
PromiseMon.resolve().then(function () {
    console.log('two'); // two
});
Copy the code

As you can see, promise.resolve is implemented in each of the three cases we listed.

The same pattern is used for promise.reject, which is not covered here

📋 Examine the past and know the new

Finally, let’s review this article with a mind map. See the picture below 👇 for details

📝 V. Conclusion

When I write here, I find that I have spent three full days, nearly 34h+, on the knowledge of Promise. Fortunately, I have finally achieved a relatively satisfactory result in the implementation of the core function of Promise.

It may be the first time for me to chew on a knowledge in such detail, so I encountered many untrod holes in the learning process. In the middle process, I recorded the problems in detail and tried to solve them, gradually perfecting a new knowledge system.

Finally, this article explains here to end! I hope you can have a better understanding of Promise

🐣 Easter Eggs One More Thing

(: Class representative records

✅ The promise. race method of 3:7. Is not implemented in the original text in code.

If the parameter is a thenable, that is, if the parameter is an object with a THEN method, then the result will return an object with a THEN method (this function is not implemented yet).

✅ The promise. reject method in 3.8. Is not implemented in the original code.

(: References

👉 [10,000 words]JavaScript asynchronous mode and Promise use

👉 [1w6k] Baby-sitter step-by-step steps to implement Promise’s core features

👉 ES6 Quick Start

(: External chapter

  • Follow the public account Monday research, the first time to pay attention to quality articles, more selected columns for you to unlock ~
  • If this article is useful to you, make sure you leave footprints before you go
  • That’s all for this article! See you next time! 👋 👋 👋