Handwriting Promise from scratch

_During interviews, Promise is often asked about; some interviewers go a little further and ask if they know how to implement Promise or if they have read the source code of Promise; today we'll see how Promise is implemented internally as a chain call.

What is Promise

_Promise is simply a container that holds the results of an event (usually an asynchronous operation) that will end in the future.Syntax, Promise is an object from which messages for asynchronous operations can be retrieved.Promise provides a unified API, and asynchronous operations can be handled in the same way.

_Promise was previously implemented through callback functions. Callback functions themselves are OK, but nesting levels are too deep and it's easy to fall into callback hell.

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 subsequent code
        });
    });
});
Copy Code

_If logical judgment or exception handling is required after each reading of the file, the entire callback function will be very complex and difficult to maintain.Promise appears to solve this pain point, so we can override the above callback nesting with Promise:

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

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

Promise specification

_promise was first proposed by the commonjs community, when a number of specifications were proposed.The promise/A specification is more acceptable.However, the promise/A specification is relatively simple, on this basis, people later proposed the promise/A+ specification, that is, the specification actually implemented in the industry; es6 also uses this specification, but es6 also joined in this specification Promise.all,Promise.race,Promise.catch,Promise.resolve,Promise.reject And other methods.

_We can use scripts to test whether the Promise we write meets the promise/A+ specification.Add the Promise we implemented to the following code:

Promise.defer = Promise.deferred = function () {
    let dfd = {};
    dfd.promise = new Promise((resolve, reject) => {
        dfd.resolve = resolve;
        dfd.reject = reject;
    });
    return dfd;
}
Copy Code

_and then throughModule.exportsExport, install scripts for tests:

npm install -g promises-aplus-tests
 Copy Code

_Execute the following commands in the directory where Promise is implemented:

promises-aplus-tests promise.js
 Copy Code

_Next, the scripts will test our scripts one by one against the promise/A+ specification.

Promise Basic Structure

_Let's review how we use Promise in general:

var p = new Promise(function(resolve, reject){
    console.log('implement')
    setTimeout(function(){
        resolve(2)
    }, 1000)
})
p.then(function(res){
    console.log('suc',res)
},function(err){
    console.log('err',err)
})
Copy Code

_First of all, Promise instantiates an object through a constructor, then processes the asynchronous returned results through the then method on the instance object.At the same time, the promise/A+ specification specifies:

promise is an object or function that has the then method and behaves in accordance with this specification.

The current state of a Promise must be one of three states: Pending, Fulfilled, and Rejected.

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

function Promise(executor) {
    var _this = this
    this.state = PENDING; //state
    this.value = undefined; //Successful results
    this.reason = undefined; //Reasons for failure
    function resolve(value) {}
    function reject(reason) {}
}

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

module.exports = Promise;
Copy Code

_When we instantiate Promise, the constructor immediately calls the incoming executor, which we can try:

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

_Therefore, in Promise the constructor executes immediately, while passing in the resolve and reject functions as parameters:

function Promise(executor) {
    var _this = this
    this.state = PENDING; //state
    this.value = undefined; //Successful results
    this.reason = undefined; //Reasons for failure
    function resolve(value) {}
    function reject(reason) {}
    executor(resolve, reject)
}
Copy Code

_But executor may also have exceptions, so try/catch captures them:

try {
    executor(resolve, reject);
} catch (e) {
    reject(e);
}
Copy Code

Invariant

The_promise/A+ specification states that when a Promise object has been changed from Pending to Fulfilled or Rejected, it cannot be changed again and the final value cannot be changed.

_Therefore, we judge in the callback functions resolve and reject that the state can only be changed if it is pending:

function resolve(value) {
    if(_this.state === PENDING){
        _this.state = FULFILLED
        _this.value = value
    }
}
function reject(reason) {
    if(_this.state === PENDING){
        _this.state = REJECTED
        _this.reason = reason
    }
}
Copy Code

_We change the state at the same time, save the successful result or the reason for the failure in the callback function in the corresponding attributes for easy access later.

then implementation

_When Promise's state changes, whether it succeeds or fails, the then callback function is triggered.Therefore, the implementation of the then is also simple, that is, to call different functions that handle the end values according to the state.

Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
};
Copy Code

_As stated in the specification, onFulfilled and onRejected are optional, so we make a type of judgment on the two values:

OnFulfilled and onRejected are optional parameters.If onFulfilled is not a function, it must be ignored.If onRejected is not a function, it must be ignored

_Code written here, it seems that there are all possible implementations. Let's write a demo test:

var myP = new Promise(function(resolve, reject){
    console.log('implement')
    setTimeout(function(){
        reject(3)
    }, 1000)
});

myP.then(function(res){
    console.log(res)
},function(err){
    console.log(err)
});
Copy Code

Goose, unfortunately, when it runs, we find that only the execution in the constructor is printed, and none of the following then functions are executed at all.Let's clean up the code to run smoothly:

_When the function inside the then runs, resolve is asynchronous and has not yet had time to modify the state, so we need to deal with the asynchronous case.

Support Asynchronous

_So how do we get our Proise to support asynchronization?We can refer to the publish subscription mode and when executing the then method, if it is still in PENDING state, store the callback function in an array, and when the state changes, remove the callback function from the array; so let's first define the variable in Promise:

function Promise(executor) {
    this.onFulfilled= []; //Successful callbacks
    this.onRejected= []; //Failed callbacks
}
Copy Code

_Thus, when the n executes, if it is still in PENDING state, we do not execute the callback function immediately, but store it:

Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
    if(this.state === PENDING){
        typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
        typeof onRejected === 'function' && this.onRejected.push(onRejected)
    }
};
Copy Code

_Once stored, it can be called when resolve or reject executes asynchronously:

function resolve(value) {
    if(_this.state === PENDING){
        _this.state = FULFILLED
        _this.value = value
        _this.onFulfilled.forEach(fn => fn(value))
    }
}
function reject(reason) {
    if(_this.state === PENDING){
        _this.state = REJECTED
        _this.reason = reason
        _this.onRejected.forEach(fn => fn(reason))
    }
}
Copy Code

_Children's shoes may ask why onFulfilled and onRejected on this side need to exist in the array, is it okay to receive them directly with one variable?Here's an example:

var p = new Promise((resolve, reject)=>{
    setTimeout(()=>{
        resolve(4)
    }, 0)
})
p.then((res)=>{
    //4 res
    console.log(res, 'res')
})
p.then((res1)=>{
    //4 res1
    console.log(res1, 'res1')
})
Copy Code

_We called then twice, and if it was a variable, it would surely end up running only the last then, overwriting the previous one. If it was an array, both thens would work correctly.

_So, when we run demo, we can see the results as expected; a simple Promise gasket of about forty lines is finished.Here's the complete code:

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'
function Promise(executor) {
    var _this = this
    this.state = PENDING; //state
    this.value = undefined; //Successful results
    this.reason = undefined; //Reasons for failure

    this.onFulfilled = [];//Successful callbacks
    this.onRejected = []; //Failed callbacks
    function resolve(value) {
        if(_this.state === PENDING){
            _this.state = FULFILLED
            _this.value = value
            _this.onFulfilled.forEach(fn => fn(value))
        }
    }
    function reject(reason) {
        if(_this.state === PENDING){
            _this.state = REJECTED
            _this.reason = reason
            _this.onRejected.forEach(fn => fn(reason))
        }
    }
    try {
        executor(resolve, reject);
    } catch (e) {
        reject(e);
    }
}
Promise.prototype.then = function (onFulfilled, onRejected) {
    if(this.state === FULFILLED){
        typeof onFulfilled === 'function' && onFulfilled(this.value)
    }
    if(this.state === REJECTED){
        typeof onRejected === 'function' && onRejected(this.reason)
    }
    if(this.state === PENDING){
        typeof onFulfilled === 'function' && this.onFulfilled.push(onFulfilled)
        typeof onRejected === 'function' && this.onRejected.push(onRejected)
    }
};
Copy Code

Chain call then

_Believe that the Promise gasket above should be easy to understand, the chain call below is the difficulty and core of Promise; let's compare the promise/A+ specification step by step, let's first see how the specification is defined:

The then method must return a promise object

promise2 = promise1.then(onFulfilled, onRejected);

_In other words, each of the then methods returns a new Promise object so that our then method can be called continuously in a chain; therefore, the then method in the simple pad above is not applicable because it does not return anything and we simply override it to return a new Promise object whatever the then does:

Promise.prototype.then = function (onFulfilled, onRejected) {
    let promise2 = new Promise((resolve, reject)=>{
    })
    return promise2
}
Copy Code

_We will continue to look at the execution of the n:

  1. If onFulfilled or onRejected returns a value of x, run the following Promise resolution: [[Resolve] (promise2, x)
  2. If onFulfilled or onRejected throws an exception e, promise2 must reject execution and return rejection e
  3. If onFulfilled is not a function and promise1 executes successfully, promise2 must execute successfully and return the same value
  4. If onRejected is not a function and promise1 refuses to execute, promise2 must refuse to execute and return the same reason

_First of all, we know that onFulfilled and onRejected will have a return value x after they are executed. Promise resolution is needed to process the return value x, let's go on; second, there's nothing to say about exception handling for onFulfilled and onRejected; third and fourth, it's really a problem, if onFulfilled and onRejectedIf two parameters are not passed, they continue to be downloaded (the transfer characteristics of values); for example:

var p = new Promise(function(resolve, reject){
    setTimeout(function(){
        resolve(3)
    }, 1000)
});
p.then(1,1)
.then('','')
.then()
.then(function(res){
    //3
    console.log(res)
})
Copy Code

_Whatever value onFulfilled and onRejected pass in here, as long as it is not a function, it will continue to pass down until a function receives it; therefore, we improve the then method as follows:

//_this is an instance object of promise1
var _this = this
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value
onRejected = typeof onRejected === 'function' ? onRejected : reason => { throw reason }

var promise2 = new Promise((resolve, reject)=>{
    if(_this.state === FULFILLED){
        let x = onFulfilled(_this.value)
        resolvePromise(promise2, x, resolve, reject)
    } else if(_this.state === REJECTED){
        let x = onRejected(_this.reason)
        resolvePromise(promise2, x ,resolve, reject)
    } else if(_this.state === PENDING){
        _this.onFulfilled.push(()=>{
            let x = onFulfilled(_this.value)
            resolvePromise(promise2, x, resolve, reject)
        })
        _this.onRejected.push(()=>{
            let x = onRejected(_this.reason)
            resolvePromise(promise2, x ,resolve, reject)
        })
    }
})
Copy Code

_We found that there is a resolvePromise in the function, which is the Promise resolution process mentioned above. It is the processing of the new promise2 and the previous execution result x. Because of the reusability, we pull it into a single function, which is also the first point defined in the specification above.

_Since the callback of the n is executed asynchronously, we need to put onFulfilled and onRejected executions into asynchronous execution and do some error handling at the same time:

//Other code omitted
if(_this.state === FULFILLED){
    setTimeout(()=>{
        try {
            let x = onFulfilled(_this.value)
            resolvePromise(promise2, x, resolve, reject)
        } catch (error) {
            reject(error)
        }
    })
} else if(_this.state === REJECTED){
    setTimeout(()=>{
        try {                    
            let x = onRejected(_this.reason)
            resolvePromise(promise2, x ,resolve, reject)
        } catch (error) {
            reject(error)
        }
    })
} else if(_this.state === PENDING){
    _this.onFulfilled.push(()=>{
        setTimeout(()=>{
            try {                        
                let x = onFulfilled(_this.value)
                resolvePromise(promise2, x, resolve, reject)
            } catch (error) {
                reject(error)
            }
        })
    })
    _this.onRejected.push(()=>{
        setTimeout(()=>{
            try {                        
                let x = onRejected(_this.reason)
                resolvePromise(promise2, x ,resolve, reject)
            } catch (error) {
                reject(error)
            }
        })
    })
}
Copy Code

Promise Resolution Process

The Promise resolution process is an abstract operation that requires entering a promise and a value, expressed as [[Resolve] (promise, x). If X has the then method and looks like a Promise, the resolver attempts to make the promise accept the state of x; otherwise, it performs the promise with the value of X.

_This is an abstract sentence. Generally speaking, the process of resolving a promise requires passing in a new promise and a value X. If the incoming x is a thenable object (with the then method), the state of X is accepted:

//promise2: New Promise object
 //x: the return value of the last then
 //resolve:resolve for promise2
 //reject:reject of promise2
function resolvePromise(promise2, x, resolve, reject) {
}
Copy Code

_After defining the function, see the specific instructions:

  1. x equals promise
    • If promise and x point to the same object, reject promise based on TypeError
  2. x is Promise
    1. If x is in the waiting state, promise needs to remain in the waiting state until x is executed or rejected
    2. If x is in the execution state, perform promise with the same value
    3. If x is in the rejection state, reject promise with the same reason
  3. x is an object or function
    1. Assign x.then to then
    2. If error E is thrown when taking the value of x.then, promise is rejected on the basis of e
    3. If the n is a function, call x as the scope of the function this.Pass in two callback functions as parameters, the first called resolvePromise and the second called rejectPromise:
      1. If resolvePromise is called with a value of y, run [[Resolve] (promise, y)
      2. If rejectPromise is called with r as the argument, promise is rejected with r as the argument
      3. If both resolvePromise and rejectPromise are called, or if they have been called multiple times by the same parameter, the first call takes precedence and the remaining calls are ignored
      4. If the n is not a function, perform promise with x as a parameter
  4. If x is not an object or function, perform promise with X as a parameter

First of all, if x and promise are equal, what is it like to go back to yourself?

var p = new Promise(function(resolve, reject){
    resolve(3)
});
//Uncaught (in promise) TypeError: Chaining cycle detected for promise #<Promise>
var p2 = p.then(function(){
    return p2
})
Copy Code

_This will lead to a dead circle, so we must first eliminate this situation:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Chaining cycle'));
    }
}
Copy Code

_Next, we will make a judgement on different situations. First, we will judge the situation where x is an object or a function:

function resolvePromise(promise2, x, resolve, reject) {
    if (promise2 === x) {
        reject(new TypeError('Chaining cycle'));
    }
    if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
        //Function or Object
    } else {
        //Normal Value
        resolve(x)
    }
}
Copy Code

_If x is an object or a function, assigning x.then to the n is understandable, but what is the second point where taking the N might make a mistake?This is because you need to take into account all the errors (just in case a kid doesn't want a gentleman), and use it when someone implements the Promise object Object.defineProperty() Malicious errors that cause the program to crash, like this:

var Promise = {};
Object.defineProperty(Promise, 'then', {
    get: function(){
        throw Error('error')
    }
})
//Uncaught Error: error
Promise.then
Copy Code

_Therefore, we also need try/catch when we get the n:

//Other code omitted
if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    //Function or Object
    try {
        let then = x.then
    } catch(e){
        reject(e)
    }
}
Copy Code

_After taking out the then, go back to 3.3 and decide if it is a function, call x as the scope of the function, and pass in two callback functions as parameters.

//Other code omitted
try {
    let then = x.then
    if(typeof then === 'function'){
        then.call(x, (y)=>{
            resolve(y)
        }, (r) =>{
            reject(r)
        })
    } else {
        resolve(x)
    }
} catch(e){
    reject(e)
}
Copy Code

_This way, our chain call can be called smoothly; however, there is a special case where if the y value of the resolve is still a Promise object, it should be continued, such as the following example:

var p1 = new Promise((resolve, reject)=>{
    resolve('p1')
})
p1.then((res)=>{
    return new Promise((resolve, reject)=>{
        resolve(new Promise((resolve, reject)=>{
            resolve('p2')
        }))
    })
})
.then((res1)=>{
    //PromiseĀ {state: "fulfilled", value: "p2"}
    console.log(res1)
})
Copy Code

_At this point the second one prints out a promise object; we should continue to recursively call resolvePromise (reference specification 3.3.1), so the complete code for the final resolvePromise is as follows:

function resolvePromise(promise2, x, resolve, reject){
    if(promise2 === x){
        reject(new TypeError('Chaining cycle'))
    }
    if(x && typeof x === 'object' || typeof x === 'function'){
        let used;
        try {
            let then = x.then
            if(typeof then === 'function'){
                then.call(x, (y)=>{
                    if (used) return;
                    used = true
                    resolvePromise(promise2, y, resolve, reject)
                }, (r) =>{
                    if (used) return;
                    used = true
                    reject(r)
                })
            } else {
                if (used) return;
                used = true
                resolve(x)
            }
        } catch(e){
            if (used) return;
            used = true
            reject(e)
        }
    } else {
        resolve(x)
    }
}
Copy Code

_Here, our Promise can also fully implement chain calls; then test the code with promises-aplus-tests and pass 872 tests perfectly.

Reference resources

promise/A specification (English) promise/A+ specification (English) promise/A+ Specification (Chinese)

For more front-end information, please pay attention to the Public Number.

If you think it's good, pay attention to meNugget Home Page .More articles please visit Xie Xiaofei's Blog

Tags: npm

Posted on Wed, 27 May 2020 21:58:09 -0400 by cowboy_x