What is the Promise

A Promise is simply a container that holds the result of an event (usually an asynchronous operation) that will end in the future. Syntactically, a Promise is an object from which to get messages for asynchronous operations. Promise provides a uniform API, and all kinds of asynchronous operations can be handled in the same way.

Promises are a solution for handling asynchronous coding. Before Promises, asynchronous code was written using callback functions. There was nothing wrong with callbacks per se, but they got complicated when multiple asynchronous callbacks had a logical relationship:

const fs = require('fs');
fs.readFile('1.txt', (err,data) => {
    fs.readFile('2.txt', (err,data) => {
        fs.readFile('3.txt', (err,data) => {
            // There may be further code
        });
    });
});
Copy the code

Three files are read above, they are hierarchical relationship, it can be seen that multiple asynchronous code set together is not vertical development, but horizontal, no matter from the syntax or from the error, so the emergence of Promise can solve this pain point.

This code would look like this if rewritten as Promise:

const util = require('util');
const fs = require('fs');
const readFile = util.promisify(fs.readFile);

readFile('1.txt')
    .then(data= > {
        return readFile('2.txt');
    }).then(data= > {
        return readFile('3.txt');
    }).then(data= > {
        / /...
    });
Copy the code

As you can see, the code is growing vertically from top to bottom, making it more logical for people.

Promise: Promises/A+ Promises: Promises/A+ Promises

Handwritten Promise is a front-end classic interview questions, such as the United States of America’s interview is a must test questions, the logic of Promise is more complex, consider the logic is more, the following summarizes the key points of handwritten Promise, and how to use the code to achieve it.

Basic Promise code structure

When you instantiate a Promise object, you pass in a function as an executor that takes two arguments (resolve and Reject) to change the result into a success state and a failure state, respectively. We can write the basic structure

function Promise(executor) {
    this.state = 'pending'; / / state
    this.value = undefined; // Successful results
    this.reason = undefined; // Failure cause

    function resolve(value) {}function reject(reason) {}}module.exports = Promise;
Copy the code

The state property stores the state of the Promise object. According to the specification, a Promise object has only three states: pending and Resolved and Rejected. When a Promise object executes successfully, it holds a result using the value attribute; It may also fail for some reason, which is stored in the Reason attribute.

Then methods are defined on stereotypes

Each Promise instance has a then method that handles the result returned asynchronously. This is the method defined on the prototype. We’ll prepare by writing an empty method:

Promise.prototype.then = function (onFulfilled, onRejected) {};Copy the code

The Promise is executed immediately when it is instantiated

It is certain that when we instantiate a Promise ourselves, its executor function will execute immediately:

let p = new Promise((resolve, reject) = > {
    console.log('Executed');
});
Copy the code

Running results:

To perform theCopy the code

Therefore, when a Promise is instantiated, the constructor immediately calls the incoming executor function

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;

    executor(resolve, reject); // Execute immediately
    
    function resolve(value) {}
    function reject(reason) {}}Copy the code

The status of a successful or failed state cannot be updated

The specification states that a Promise object cannot change its state again when it has changed from pending to resolved or Rejected. Therefore, when updating the state, we need to check if the current state is pending:

    function resolve(value) {
        // Update pending status
        if (_this.state === 'pending') {
            _this.value = value;// Save the result successfully
            _this.state = 'resolved'; }}function reject(reason) {
    // Update pending status
        if (_this.state === 'pending') {
            _this.reason = reason;// Cause of the save failure
            _this.state = 'rejected'; }}Copy the code

As you can see above, the resolve and reject functions are added to determine whether the operation can be performed only when the current state is pending, and the result of success and the cause of failure are saved to the corresponding attribute. The state property is then set to the updated state.

Basic implementation of then method

The then method is called when the state of a Promise changes, whether it succeeds or fails. Therefore, it is easy to implement the THEN method by calling different callback functions based on the state:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if (this.state === 'resolved') {
        // Determine the type of argument that the function executes
        if (typeof onFulfilled === 'function') {
            onFulfilled(this.value); }}if (this.state === 'rejected') {
        if (typeof onRejected === 'function') {
            onRejected(this.reason); }}};Copy the code

One thing to note is that onFulfilled and onRejected are both optional parameters, that is, they can be fulfilled or not fulfilled. The callback passed in is also not a function type, so what happens? The specification says ignore it. Therefore, you need to determine the type of the callback and execute it if it is a function.

Make Promise asynchronous

If a Promise encapsulates an asynchronous operation, the then method will not be able to do so:

let p = new Promise((resolve, reject) = > {
    setTimeout((a)= > {
        resolve(1); },500);
});

p.then(data= > console.log(data)); // There is no result
Copy the code

If I run the code above, I find nothing. If I wait 500 milliseconds, I execute the then method. The reason is that setTimeout function makes resolve execute asynchronously with delay. When the THEN method is called, the state at this moment is still pending. Therefore, the then method neither calls ondepressing nor onRejected.

How can this problem be solved? We may follow a subscription model, then the execution method if the state is still waiting for (pending), the callback function temporary deposit into an Array, when the state changes from an Array in order to retrieve the execution, clear the idea we achieve it, first in the class two Array of new types of arrays, used to hold the callback function:

function Promise(executor) {
    var _this = this;
    this.state = 'pending';
    this.value = undefined;
    this.reason = undefined;
    this.onFulfilledFunc = [];// Callback saved successfully
    this.onRejectedFunc = [];// Save failed callback
    // Other code is omitted...
}
Copy the code

So when the then method executes and the state is pending, place the callbacks in the array one by one:

Promise.prototype.then = function (onFulfilled, onRejected) {
    // Wait state, when the asynchronous code has not finished
    if (this.state === 'pending') {
        if (typeof onFulfilled === 'function') {
            this.onFulfilledFunc.push(onFulfilled);// Save the callback
        }
        if (typeof onRejected === 'function') {
            this.onRejectedFunc.push(onRejected);// Save the callback}}// Other code is omitted...
};
Copy the code

With the callback stored, it’s time to execute when the state changes:

    function resolve(value) {
        if (_this.state === 'pending') {
            _this.value = value;
            // Execute successful callbacks in sequence
            _this.onFulfilledFunc.forEach(fn= > fn(value));
            _this.state = 'resolved'; }}function reject(reason) {
        if (_this.state === 'pending') {
            _this.reason = reason;
            // Execute failed callbacks in sequence
            _this.onRejectedFunc.forEach(fn= > fn(reason));
            _this.state = 'rejected'; }}Copy the code

At this point, promises have support for asynchronous operations, and setTimeout delays can be used to properly execute the then method to return results.

Chain calls

The most powerful part of Promise’s handling of asynchronous code is its support for chained calls, which are also the most complex. Let’s take a look at what the specification defines:

  1. Each then method returns a new Promise object (the heart of the principle)
  2. If the then method explicitly returns a Promise object, that object is returned
  3. If the then method returns a common value (Number, String, etc.), it returns a new Promise object wrapped with that value.
  4. If there is no return statement in the then method, it is treated as returning a Promise object wrapped in Undefined
  5. If an exception occurs in the THEN method, reject is called to jump to the onRejected of the next THEN
  6. If the THEN method does not pass any callbacks, it continues passing (the passing nature of the value).

The specification is very similar, we can use the code to demonstrate the points that are not easy to understand.

The third item is wrapped as a Promise if it returns a normal value, which we demonstrate in code:

let p =new Promise((resolve,reject) = >{
    resolve(1);
});

p.then(data= >{
    return 2; // Return a normal value
}).then(data= >{
    console.log(data); 2 / / output
});
Copy the code

It can be seen that when a common value is returned by then, the return result of the previous THEN can be retrieved in the successful state callback of the next THEN, indicating that the previous THEN is a Promise wrapped with 2, which conforms to the specification.

Item 4: If there is no return statement in the then method, it is treated as returning a Promise object wrapped in Undefined

let p = new Promise((resolve, reject) = > {
    resolve(1);
});

p.then(data= > {
    // No return statement
}).then(data= > {
    console.log(data); //undefined
});
Copy the code

As you can see, no error is reported when no value is returned, and no statement is actually return undefined; The successful state that wraps undefined as a Promise object and passes it to the next THEN.

Item 6: If the then method passes no callbacks, it passes further down. What does that mean? This is the penetration of the Promise value, again in code:

let p = new Promise((resolve, reject) = > {
    resolve(1);
});

p.then(data= > 2)
.then()
.then()
.then(data= > {
    console.log(data); / / 2
});
Copy the code

The code above calls two empty THEN methods in a row after the first one, passing no callbacks and returning no values, at which point the Promise passes the value down until you receive it. This is called value penetration.

Now you can see how the chain calls work. In either case, the THEN method returns a Promise object, which leads to the next THEN method.

Knowing these points, we can implement a chain call to the THEN method to improve it:

Promise.prototype.then = function (onFulfilled, onRejected) {
    var promise2 = new Promise((resolve, reject) = > {
    //...
    }
    return promise2;
};
Copy the code

First, if then returns a Promise object in either case, we instantiate a new promise2 and return it.

We then deal with generating a new Promise object based on the return value of the previous THEN method. Since this logic is complex and has many calls, we pull out a method to operate on it, which is also stated in the specification:

/** * resolve then return value with new Promise Object * @param {Object} promise2 new Promise Object * @param {*} x then return value * @param {Function} resolve Resolve * @param {Function} reject */
function resolvePromise(promise2, x, resolve, reject) {
    / /...
}
Copy the code

If x and promise2 point to the same object, TypeError is used as a cause of failure. The original text reads as follows:

If promise and x refer to the same object, reject promise with a TypeError as the reason.

What does that mean? If then returns the same value as the newly generated Promise (at the same reference address), TypeError will be raised:

let promise2 = p.then(data= > {
    return promise2;
});
Copy the code

Running results:

TypeError: Chaining cycle detected for promise #<Promise>
Copy the code

Obviously, if you return your Promise object, the state is always pending and can no longer be Resolved or Rejected, the program will die, so deal with it first:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('The Promise has a circular reference.')); }}Copy the code

The next step is to deal with each situation. When X is a Promise, implement it, success is success, failure is failure. If x is an object or function, deal with it further, otherwise it is a normal value:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('The Promise has a circular reference.'));
    }

    if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
        // It can be an object or a function
    } else {
        // Otherwise a normal valueresolve(x); }}Copy the code

If an error is reported, the promise2 state is set to fail. The original:

If retrieving the property x.then results in a thrown exception e, reject promise with e as the reason.

function resolvePromise(promise2, x, resolve, reject) {
    //...
    if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
        // It can be an object or a function
        try {
            let then = x.then;// Retrieve the then method reference
        } catch(e) { reject(e); }}else {
        // Otherwise a normal valueresolve(x); }}Copy the code

More on that, why is it possible to get an error when you take a property on an object? Promises have many implementations (Bluebird, Q, etc.). Promises/A+ is just A standard. Promises can only be universal if everyone implements them according to this standard. If someone else implements a Promise Object that uses Object.defineProperty() to maliciously throw values incorrectly, we can prevent bugs in our code.

At this point, if the object has then and then is a function type, it is considered a Promise object, and then is called using x as this.

If then is a function, call it with x as this

// Other code is omitted...
if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
    // It can be an object or a function
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            //then is function, then implement Promise
            then.call(x, (y) => {
                resolve(y);
            }, (r) => {
                reject(r);
            });
        } else{ resolve(x); }}catch(e) { reject(e); }}else {
    // Otherwise a normal value
    resolve(x);
}
Copy the code

So we’re almost done with the chain. At the extreme, however, if the Promise object turns successful or fails with another Promise object, execution should continue until the last Promise has been executed.

p.then(data= > {
    return new Promise((resolve,reject) = >{
        //resolve is passed as a Promise
        resolve(new Promise((resolve,reject) = >{
            resolve(2);
        }));
    });
})
Copy the code

This is where the recursive operation comes in.

The specification reads as follows:

If a promise is resolved with a thenable that participates in a circular thenable chain, such that the recursive nature of [[Resolve]](promise, thenable) eventually causes [[Resolve]](promise, thenable) to be called again, following the above algorithm will lead to infinite recursion. Implementations are encouraged, but not required, to detect such recursion and reject promise with an informative TypeError as the reason.

Simply rewrite the call resolve to recursively execute the resolvePromise method, which does not terminate until the Promise is resolved to a normal value:

// Other code is omitted...
if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
    // It can be an object or a function
    try {
        let then = x.then; 
        if (typeof then === 'function') {
            let y = then.call(x, (y) => {
                // Call recursively, passing in y as a Promise object and continuing the loop
                resolvePromise(promise2, y, resolve, reject);
            }, (r) => {
                reject(r);
            });
        } else{ resolve(x); }}catch(e) { reject(e); }}else {
    // is a normal value, which ends the recursion
    resolve(x);
}

Copy the code

At this point, the code for the chain call is complete. Call the resolvePromise method where appropriate.

The last

Actually, the actual source code for Promise has been written here, but it is still one point away from 100 points. What is it?

The specification states that Promise’s then methods are executed asynchronously.

onFulfilled or onRejected must not be called until the execution context stack contains only platform code.

ES6’s native Promise objects already implement this, but our own code executes synchronously, so how do we make synchronous code asynchronous? This can be simulated using the setTimeout function:

setTimeout((a)= >{
    // The code is executed asynchronously
},0);
Copy the code

Use this technique to make all parts of the code then execution asynchronous using setTimeout, for example:

setTimeout((a)= > {
    try {
        let x = onFulfilled(value);
        resolvePromise(promise2, x, resolve, reject);
    } catch(e) { reject(e); }},0);
Copy the code

Ok, now we have the full Promise source code.

Full score test

The open-source community provided A package to test Promises/ Aplus-tests to see if the promised source code was truly Promises/A+ compliant

The usage method of this package is not detailed, this package can check the compliance of our code item by item, if there is any inconsistency will be reported to us, if your code is green all the way, congratulations, your Proimse has been legal, can be online for others to use:

872 tests passed!

Now the source code will be written, finally can confidently answer the interviewer’s questions.