Updated instructions

  • Last update at: 2019/1/23

I made the execution of the THEN method synchronous, which is not in compliance with the specification.

[6] : “onFulfilled and onRejected can only be called when the implementation environment stack only contains the platform code.” This is A big pity.

Therefore, I will place the onFulfilled and onRejected codes in “the new execution stack after the event cycle in which the THEN method is called”, and put the task at the end of the task queue of this round through setTimeout method. The code has been added to the last section – step 9.

If you are interested in the operation mechanism of task queue, please refer to Ruan Yifeng’s detailed Explanation of JavaScript Operation Mechanism: Event Loop Again.


Functions:

  • The implementedPromiseBasic functions, like the original, asynchronous and synchronous operations are ok, including:
    • MyPromise.prototype.then()
    • MyPromise.prototype.catch()With nativePromiseSlight discrepancy
    • MyPromise.prototype.finally()
    • MyPromise.all()
    • MyPromise.race()
    • MyPromise.resolve()
    • MyPromise.reject()
  • rejectedThe bubbling of the state has also been resolved, with the current Promise’s Reject bubbling until the end, until the catch, if not caught
  • MyPromiseOnce a state has changed, it cannot change its state

Disadvantages:

  • When an error in your code is caught by a catch, you are prompted with more information (the error object caught) than the original Promise
  • The code is written in ES6 and will be considered to be written in ES5 for easy application in ES5 projects; In ES5, instead of using the arrow function, you have to worry about this

Testing:index.html

  • This page contains 27 test examples, testing each feature, each method, and some special case tests; Perhaps there are omissions, interested in their own can play;
  • Visual operation, convenient test, each run an example, open the debugging platform can see the results; It is recommended to open it simultaneously.index.jsPlay while looking at code;
  • Same code, up hereMyPromiseThe following is the nativePromiseThe result of operation;

harvest

  • This process is very happy, can challenge the original things by myself, this is my first time;
  • It took days to do thatPromiseFirst understand him, then to think about him, finally step by step to implement the function, his understanding deepened, more and more thorough;
  • When I write a new function, I find that the previous function is missing in this new function. At this time, I need to understand the relationship between them and re-understand the previous function. In this repetition a new level of understanding has undoubtedly been deepened;
  • then/catchThe method is the most difficult, tinkering;
  • Finally, after all the functions were implemented, I realized the key point “Once the Promise state is determined, it cannot be changed”, and added some logic to solve the problem. So, this process, it’s hard to be perfect, maybe there are some hidden problems in the current code that haven’t been discovered.
  • rejectBubbling of state is a problem, but I didn’t specifically mention it in the code below, and I didn’t have a way to specify it. I kept tuning throughout the whole process until I finally got the right bubbling result.

code

The following snippet of code, including the whole thought process, will be a bit long. In order to illustrate the logic of writing, I use the following comments to indicate that the whole mess of changing code only identifies the beginning of the mess. //++ — added code //-+ — modified code

First, define the MyPromise class

Whatever the name is, mine is MyPromise, not replacing the original Promise.

  • The constructor passes in the callback functioncallback. When a newMyPromiseObject, we need to run this callback, andcallbackIt also has two parameters of itselfresolvereject, they are also the form of callback functions;
  • Several variables are defined to hold some of the current results and status, event queues, see comments;
  • Executive functioncallbackIf yesresolveState to save the result inthis.__succ_res, the status is marked as success; If it isrejectState, operation similar;
  • It also defines the most commonly usedthenMethod, is a prototype method;
  • performthenMethod, determine whether the state of the object is successful or failed, execute the corresponding callback respectively, and pass the result into the callback processing;
  • Here to receive. argAnd pass in parameters. this.__succ_resBoth use extended operators that are passed unchanged to handle multiple argumentsthenMethod callback.

The callback uses the arrow function, and this points to the current MyPromise object, so you don’t have to deal with this.

class MyPromise {
    constructor(callback) {
        this.__succ_res = null;     // Save the result successfully
        this.__err_res = null;      // Save the result of the failure
        this.status = 'pending';    // Marks the status of the processing
        // The arrow function is bound to this. If you use es5, you need to define an alternative this
        callback((. arg) = > {
            this.__succ_res = arg;
            this.status = 'success'; }, (... arg) => {this.__err_res = arg;
            this.status = 'error';
        });
    }
    then(onFulfilled, onRejected) {
        if (this.status === 'success') { onFulfilled(... this.__succ_res); }else if (this.status === 'error') {
            onRejected(...this.__err_res);
        };
    }
};
Copy the code

From here, MyPromise can simply implement some synchronization code, such as:

new MyPromise((resolve, reject) = > {
    resolve(1);
}).then(res= > {
    console.log(res);
});
1 / / results
Copy the code

The second step is to add asynchronous processing

When asynchronous code is executed, the then method is executed before the asynchronous result, which is not yet available to the above processing.

  • First of all, since it’s asynchronous,thenMethods in thependingState, so add oneelse
  • performelse“, we don’t have the result yet, so we can just put the callback that needs to be executed in a queue and execute it when we need to, so we define a new variablethis.__queueSave the event queue;
  • When the asynchronous code has finished executing, this time thethis.__queueAll callbacks in the queue are executed, if yesresolveIf the command is in the state, run the corresponding commandresolveThe code.
class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     // Save the result successfully
        this.__err_res = null;      // Save the result of the failure
        this.status = 'pending';    // Marks the status of the processing
        this.__queue = [];          // Event queue //++
        // The arrow function is bound to this. If you use es5, you need to define an alternative this
        fn((. arg) = > {
            this.__succ_res = arg;
            this.status = 'success';
            this.__queue.forEach(json= > {			//++json.resolve(... arg); }); }, (... arg) => {this.__err_res = arg;
            this.status = 'error';
            this.__queue.forEach(json= > {			//++json.reject(... arg); }); }); } then(onFulfilled, onRejected) {if (this.status === 'success') { onFulfilled(... this.__succ_res); }else if (this.status === 'error') { onRejected(... this.__err_res); }else {										//++
            this.__queue.push({resolve: onFulfilled, reject: onRejected}); }; }};Copy the code

At this point, MyPromise is ready to implement some simple asynchronous code. Both examples are already available in the test case index.html.

  • 1 Asynchronous test --resolve
  • 2 Asynchronous test -- Reject

Third, add the chain call

In fact, the then method of the native Promise object also returns a Promise object, a new Promise object, so that it can support chain calls, and so on… Furthermore, the THEN method can receive the result of the return processed by the previous THEN method. According to the feature analysis of the Promise, this return result has three possibilities:

  1. MyPromiseObject;
  2. withthenMethod object;
  3. Other values. Each of these three cases is treated separately.
  • The first one is,thenMethod returns aMyPromiseObject received by its callback functionresFnandrejFnTwo callback functions;
  • Encapsulate the handling code for the success status ashandleFunction, which takes the result of success as an argument;
  • handleIn the function, according toonFulfilledReturns different values, do different processing:
    • First, getonFulfilledThe return value (if any) of thereturnVal;
    • Then, judgereturnValIs there a then method that includes cases 1 and 2 discussed above (it isMyPromiseObject, or hasthenOther objects of a method) are the same to us;
    • And then, if there arethenMethod, which is called immediatelythenMethod, throw the results of success and failure to the newMyPromiseObject callback function; No result is passedresFnCallback function.
class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     // Save the result successfully
        this.__err_res = null;      // Save the result of the failure
        this.status = 'pending';    // Marks the status of the processing
        this.__queue = [];          // Event queue
        // The arrow function is bound to this. If you use es5, you need to define an alternative this
        fn((. arg) = > {
            this.__succ_res = arg;
            this.status = 'success';
            this.__queue.forEach(json= >{ json.resolve(... arg); }); }, (... arg) => {this.__err_res = arg;
            this.status = 'error';
            this.__queue.forEach(json= >{ json.reject(... arg); }); }); } then(onFulfilled, onRejected) {return new MyPromise((resFn, rejFn) = > {							//++
            if (this.status === 'success') { handle(... this.__succ_res);//-+
            } else if (this.status === 'error') { onRejected(... this.__err_res); }else {
               this.__queue.push({resolve: handle, reject: onRejected});        //-+
            };
            function handle(value) {									//++
                // Then method ondepressing. If there is a return, the value of return will be used; if there is no return, the saved value will be used
                let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
                // If onFulfilled returns a new MyPromise object or an object with a THEN method, its THEN method will be called
                if (returnVal && returnVal['then'] instanceof Function) {
                    returnVal.then(res= > {
                        resFn(res);
                    }, err => {
                        rejFn(err);
                    });
                } else {/ / other valuesresFn(returnVal); }; }; })}};Copy the code

At this point, MyPromise objects already support chained calls. Test example: 4 chained calls –resolve. But, obviously, we’re not done chain-calling the Reject state. Check if the onRejected return result contains the THEN method. Then check if the onRejected return result contains the THEN method. It is important to note that resFn should be called, not rejFn, if the returned value is a normal value, because the returned value belongs to the new MyPromise object and its state is not determined by the current MyPromise object state. That is, the normal value is returned, reject is not indicated, and we default to resolve.

Code is too long and only changes are shown.

then(onFulfilled, onRejected) {
    return new MyPromise((resFn, rejFn) = > {
        if (this.status === 'success') { handle(... this.__succ_res); }else if (this.status === 'error') { errBack(... this.__err_res);//-+
        } else {
           this.__queue.push({resolve: handle, reject: errBack});			//-+
        };
        function handle(value) {
            // Then method ondepressing. If there is a return, the value of return will be used; if there is no return, the saved value will be used
            let returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
            // If onFulfilled returns a new MyPromise object or an object with a THEN method, its THEN method will be called
            if (returnVal && returnVal['then'] instanceof Function) {
                returnVal.then(res= > {
                    resFn(res);
                }, err => {
                    rejFn(err);
                });
            } else {/ / other values
                resFn(returnVal);
            };
        };
        function errBack(reason) {										//++
	        if (onRejected instanceof Function) {
	        	// If there is an onRejected callback, execute it once
	            let returnVal = onRejected(reason);
	            // If the onRejected callback returns, check whether the thenable object is enabled
	            if (typeofreturnVal ! = ='undefined' && returnVal['then'] instanceof Function) {
	                returnVal.then(res= > {
	                    resFn(res);
	                }, err => {
	                    rejFn(err);
	                });
	            } else {
	                // No return or not thenable is thrown directly to the new object resFn callback
	                resFn(returnVal);				//resFn instead of rejFn
	            };
	        } else {// Pass to the next reject callbackrejFn(reason); }; }; })}Copy the code

The MyPromise object now supports chain calls nicely. Test example:

  • 4 chain call --resolve
  • 5 Chain call --reject
  • 28 Then callback returns a Promise object (Reject)
  • The then method reject returns a Promise object

The fourth step, myPromise.resolve () and myPromise.reject () methods are implemented

Because other methods have dependencies on myPromise.resolve (), implement this method first. Resolve () : myPromise.resolve () : myPromise.resolve () : myPromise.resolve () : myPromise.resolve () : myPromise.resolve () : myPromise.resolve () : The key point lies in the form of parameters, which are as follows:

  • The parameter is aMyPromiseInstance;
  • The parameter is athenableObject;
  • Parameter does not havethenMethod object, or not object at all;
  • It takes no parameters.

The processing idea is:

  • First consider the extreme case, the parameter is undefined or null, directly handle the original value pass;
  • Second, the argument isMyPromiseInstance, no action is required;
  • And then, the parameters are something elsethenableObject, which is calledthenMethod to pass the corresponding value to newMyPromiseObject callback;
  • Finally, there is the processing of ordinary values.

The myPromise.reject () method is much simpler. Unlike the myPromise.resolve () method, the arguments to myPromise.reject () are left as reject arguments to subsequent methods.

MyPromise.resolve = (arg) = > {
    if (typeof arg === 'undefined' || arg == null) {// No argument /null
        return new MyPromise((resolve) = > {
            resolve(arg);
        });
    } else if (arg instanceof MyPromise) {
        return arg;
    } else if (arg['then'] instanceof Function) {
        return new MyPromise((resolve, reject) = > {
            arg.then((res) = > {
                resolve(res);
            }, err => {
                reject(err);
            });
        });
    } else {
        return new MyPromise(resolve= >{ resolve(arg); }); }}; MyPromise.reject =(arg) = > {
    return new MyPromise((resolve, reject) = > {
        reject(arg);
    });
};
Copy the code

There are 8 test cases: 18-25, you can play with them if you are interested.

Fifth, the myPromise.all () and myPromise.race () methods are implemented

The myPromise.all () method takes a bunch of MyPromise objects and executes the callback when they all succeed. Rely on the myPromise.resolve () method to convert arguments that are not MyPromise to MyPromise objects. Each object executes the then method, storing the results in an array, and then calls the resolve() callback to pass in the results when they are all done, I === arr.length. The myPromise.race () method is similar, except that it makes a done flag, and if one of them changes state, no other changes are accepted.

MyPromise.all = (arr) = > {
    if (!Array.isArray(arr)) {
        throw new TypeError('The argument should be an array! ');
    };
    return new MyPromise(function(resolve, reject) {
        let i = 0, result = [];
        next();
        function next() {
            // If it is not a MyPromise object, it needs to be converted
            MyPromise.resolve(arr[i]).then(res= > {
                result.push(res);
                i++;
                if (i === arr.length) {
                    resolve(result);
                } else{ next(); }; }, reject); }; })}; MyPromise.race =arr= > {
    if (!Array.isArray(arr)) {
        throw new TypeError('The argument should be an array! ');
    };
    return new MyPromise((resolve, reject) = > {
        let done = false;
        arr.forEach(item= > {
            // If it is not a MyPromise object, it needs to be converted
            MyPromise.resolve(item).then(res= > {
                if(! done) { resolve(res); done =true;
                };
            }, err => {
                if(! done) { reject(err); done =true; }; }); })})}Copy the code

Test cases:

  • 6 all methods
  • 26 Race method test

Step 6, Promise. Prototype. The catch () and Promise. The prototype, the finally () method

They are essentially an extension of the THEN method, a special case treatment. The comment part of the catch code was my original solution: run the callback if the catch is already in an error state; If it is any other state, the callback function is pushed into the event queue until the previous reject state is finally received. Because catch accepts reject, resolve is empty to prevent an error. Later I read reference article 3 and realized that there was a better way to write it, so I replaced it.

class MyPromise {
	constructor(fn) {
		/ /... slightly
	}
    then(onFulfilled, onRejected) {
    	/ /... slightly
	}
    catch(errHandler) {
        // if (this.status === 'error') {
        // errHandler(... this.__err_res);
        // } else {
        // this.__queue.push({resolve: () => {}, reject: errHandler});
        // // pushes an empty function into the resolve queue when the last Promise is processed. ---- raises an error if there is no Promise
        // };
        return this.then(undefined, errHandler);
    }
    finally(finalHandler) {
        return this.then(finalHandler, finalHandler); }};Copy the code

Test cases:

  • 7 the catch test
  • 16 finally test -- Asynchronous code error
  • 17 finally test -- Synchronization code error

Step 7: Catch code errors

Currently, our catch does not have the ability to catch code errors. Think, where does the wrong code come from? Must be the user’s code, two sources are respectively:

  • MyPromiseObject constructor callback
  • thenMethod’s two callbacks to catch code running errors are nativetry... catch..., so I use it to wrap these callback runs around, and the errors caught are handled accordingly.

Resolver and rejecter functions have been extracted to ensure clarity of the code. Since it is written in ES5, the this pointing problem needs to be handled manually

class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     // Save the result successfully
        this.__err_res = null;      // Save the result of the failure
        this.status = 'pending';    // Marks the status of the processing
        this.__queue = [];          // Event queue
        // Defining function requires manual handling of this pointing
        let _this = this;									//++
        function resolver(. arg) {							//++
            _this.__succ_res = arg;
            _this.status = 'success';
            _this.__queue.forEach(json= >{ json.resolve(... arg); }); };function rejecter(. arg) {							//++
	        _this.__err_res = arg;
	        _this.status = 'error';
	        _this.__queue.forEach(json= >{ json.reject(... arg); }); };try {												//++
       		fn(resolver, rejecter);							//-+
        } catch(err) {										//++
            this.__err_res = [err];
            this.status = 'error';
            this.__queue.forEach(json= >{ json.reject(... err); }); }; } then(onFulfilled, onRejected) {// The arrow function is bound to this. If you use es5, you need to define an alternative this
        return new MyPromise((resFn, rejFn) = > {
            function handle(value) {
                // Then (ondepressing) if there is a return, the value of return will be used; if there is no return, the value of resolve will be used
                let returnVal = value;						//-+
                if (onFulfilled instanceof Function) {		//-+
                    try {									//++
                        returnVal = onFulfilled(value);
                    } catch(err) {							//++
                        // Code error handling
                        rejFn(err);
                        return; }};if (returnVal && returnVal['then'] instanceof Function) {
                	// If onFulfilled returns a new Promise object, its then method will be called
                    returnVal.then(res= > {
                        resFn(res);
                    }, err => {
                        rejFn(err);
                    });
                } else {
                    resFn(returnVal);
                };
            };
            function errBack(reason) {
                // If there is an onRejected callback, execute it once
                if (onRejected instanceof Function) {
                    try {													//++
                        let returnVal = onRejected(reason);
                        // If the onRejected callback returns, check whether the thenable object is enabled
                        if (typeofreturnVal ! = ='undefined' && returnVal['then'] instanceof Function) {
                            returnVal.then(res= > {
                                resFn(res);
                            }, err => {
                                rejFn(err);
                            });
                        } else {
                            // Not thenable, just throw the new object resFn callback
                            resFn(returnVal);
                        };
                    } catch(err) {											//++
                        // Code error handling
                        rejFn(err);
                        return; }}else {// Pass to the next reject callback
                    rejFn(reason);
                };
            };
            if (this.status === 'success') { handle(... this.__succ_res); }else if (this.status === 'error') { errBack(... this.__err_res); }else {
                this.__queue.push({resolve: handle, reject: errBack}); }; })}};Copy the code

Test cases:

  • Catch test -- code error capture
  • 12 Catch tests -- Code error catching (asynchronous)
  • Catch test -- then callback code error catch
  • 14 catch tests -- code error catch

The 12th asynchronous code error test showed a direct error and no error was caught, as was the native Promise, and I couldn’t understand why it wasn’t caught.

Step 8, handle that the MyPromise state is not allowed to change again

This is a key Promise feature, and it’s not hard to handle. Add a state determination when a callback is executed, and if it’s already in a successful or failed state, the callback code doesn’t run.

class MyPromise {
    constructor(fn) {
        this.__succ_res = null;     // Save the result successfully
        this.__err_res = null;      // Save the result of the failure
        this.status = 'pending';    // Marks the status of the processing
        this.__queue = [];          // Event queue
        // The arrow function is bound to this. If you use es5, you need to define an alternative this
        let _this = this;
        function resolver(. arg) {
            if (_this.status === 'pending') {				//++
                // If the state has changed, no longer execute this code
                _this.__succ_res = arg;
                _this.status = 'success';
                _this.__queue.forEach(json= >{ json.resolve(... arg); }); }; };function rejecter(. arg) {
            if (_this.status === 'pending') {				//++
                // If the state has changed, no longer execute this code
                _this.__err_res = arg;
                _this.status = 'error';
                _this.__queue.forEach(json= >{ json.reject(... arg); }); }; };try {
            fn(resolver, rejecter);
        } catch(err) {
            this.__err_res = [err];
            this.status = 'error';
            this.__queue.forEach(json= >{ json.reject(... err); }); }; }/ /... slightly
};
Copy the code

Test cases:

  • 27 The Promise state changes several times

The ninth step, onFulfilled and onRejected methods are implemented asynchronously

So far, if I execute the following code,

function test30() {
  function fn30(resolve, reject) {
      console.log('running fn30');
      resolve('resolve @fn30')};console.log('start');
  let p = new MyPromise(fn30);
  p.then(res= > {
      console.log(res);
  }).catch(err= > {
      console.log('err=', err);
  });
  console.log('end');
};
Copy the code

The output is:

/ / MyPromise results
// start
// running fn30
// resolve @fn30
// end

// Original Promise result:
// start
// running fn30
// end
// resolve @fn30
Copy the code

The two results are different. Because the onFulfilled and onRejected methods are not implemented asynchronously, the following processing needs to be done: put their code to the end of the task queue of this round and execute it.

function MyPromise(callback) {
    / / a little...

    var _this = this;
    function resolver(res) {
        setTimeout((a)= > {      //++ use setTimeout to adjust the task execution queue
            if (_this.status === PENDING) {
                _this.status = FULFILLED;
                _this.__succ__res = res;
                _this.__queue.forEach(item= > {
                    item.resolve(res);
                });
            };            
        }, 0);
    };
    function rejecter(rej) {
        setTimeout((a)= > {      //++
            if (_this.status === PENDING) {
                _this.status = REJECTED;
                _this.__err__res = rej;
                _this.__queue.forEach(item= > {
                    item.reject(rej);
                });
            };            
        }, 0);
    };
    
    / / a little...
};
Copy the code

Test cases:

  • 30 Asynchronous execution of the then method

Above, is all my code writing ideas, process. Complete code and test code to github download


Refer to the article

  • ECMAScript 6 Getting Started – Promise objects
  • Es6 Promise source code implementation
  • Give me a hand to make a full Promise
  • Detailed explanation of JavaScript operation mechanism: Talk about Event Loop again