preface

I read some articles about ASYNCHRONOUS JS operations a while back and found that promises are a great thing to use with Generator or async/await. The perfect solution to the callback problem of asynchronous code writing helps to write more elegant asynchronous code. Spent a few days to study the working mechanism of Promise, hand Itch encapsulated a Promise object with ES6 grammar, basically realized the function of the original Promise, now, write again with ES5 grammar.


Updated instructions

  • Last update at: 2019/1/23

By @logbn520, I have made the execution of then method synchronous, which is not in accordance with the specification.

OnFulfilled and onRejected can only be called when the implementation environment stack only contains the platform code. “Promises/A+ Specification”, “onFulfilled and onRejected” can be called only when the implementation environment stack contains only the platform code.

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

Testing:index.html

  • This page contains 30 test examples that test each feature, each method, and some special case tests; Perhaps there are omissions, interested in their own can play;
  • More user-friendly visual operation, easy to test, run one example at a time, the right panel can see the results;
  • The customconsole.mylog()The method is used to output the result, with the first argument being the one currently in usePromiseObject, to distinguish between output, view the code can be ignored, the following parameters are output results, and the systemconsole.log()Similar;
  • 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

  • Write again and have new harvest, write more smoothly, understand more deeply;
  • then/catchThe method is the most difficult, tinkering;
  • 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

The first step is the realization of basic functions

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 itselfresolverrejecter, 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 to determine whether the state of the object is successful or failed, execute the corresponding callback respectively, and pass the result to the callback processing.
// Several state constants
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(callback) {
    this.status = PENDING; // Store state
    this.__succ__res = null; // Save the resolve result
    this.__err__res = null; // save the reject result
    var _this = this;// The reference to this must be handled
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
    };
    callback(resolver, rejecter);
};
MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
        onFulfilled(this.__succ__res);
    } else if (this.status === REJECTED) {
        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.
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';

function MyPromise(callback) {
    this.status = PENDING; // Store state
    this.__succ__res = null; // Save the resolve result
    this.__err__res = null; // save the reject result
    this.__queue = []; //++ event queue

    var _this = this;
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
        _this.__queue.forEach(item= > {//++ execution of events in the queue
            item.resolve(res);
        });
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
        _this.__queue.forEach(item= > {//++ execution of events in the queue
            item.reject(rej);
        });
    };
    callback(resolver, rejecter);
};

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
        onFulfilled(this.__succ__res);
    } else if (this.status === REJECTED) {
        onRejected(this.__err__res);
    } else {// add queue events to pending state
        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 ashandleFulfilledFunction, which takes the result of success as an argument;
  • handleFulfilledIn 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.

Citation: Chain calls with reject are treated similarly; in the handleRejected function, check whether the result returned by onRejected contains the THEN method, treated separately. 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.

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    var _this = this;
    return new MyPromise(function(resFn, rejFn) {
        if (_this.status === FULFILLED) {
            handleFulfilled(_this.__succ__res);     // -+
        } else if (_this.status === REJECTED) {
            handleRejected(_this.__err__res);       // -+
        } else {/ / pending state
            _this.__queue.push({resolve: handleFulfilled, reject: handleRejected}); // -+
        };

        function handleFulfilled(value) {   // this is a big pity
            // Depends on the return value of onFulfilled
            var returnVal = onFulfilled instanceof Function && onFulfilled(value) || value;
            if (returnVal['then'] instanceof Function) {
                returnVal.then(function(res) {
                    resFn(res);
                },function(rej) {
                    rejFn(rej);
                });
            } else {
                resFn(returnVal);
            };
        };
        function handleRejected(reason) {   // ++ REJECTED State callback
            if (onRejected instanceof Function) {
                var returnVal = onRejected(reason);
                if (typeofreturnVal ! = ='undefined' && returnVal['then'] instanceof Function) {
                    returnVal.then(function(res) {
                        resFn(res);
                    },function(rej) {
                        rejFn(rej);
                    });
                } else {
                    resFn(returnVal);
                };
            } else {
                rejFn(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. The myPromise.resolve () method is described in yifeng Ruan’s ECMAScript 6 introduction. The function of this method is to convert the parameter into a MyPromise object. The key is the form of the parameter.

  • 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 = function(arg) {
    if (typeof arg === 'undefined' || arg === null) {   / / undefined or null
        return new MyPromise(function(resolve) {
            resolve(arg);
        });
    } else if (arg instanceof MyPromise) {      // The argument is MyPromise instance
        return arg;
    } else if (arg['then'] instanceof Function) {   // The argument is the thenable object
        return new MyPromise(function(resolve, reject) {
            arg.then(function (res) {
                resolve(res);
            }, function (rej) {
                reject(rej);
            });
        });
    } else {    / / other values
        return new MyPromise(function (resolve) {
            resolve(arg);
        });
    };
};
MyPromise.reject = function(arg) {
    return  new MyPromise(function(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 = function(arr) {
    if (!Array.isArray(arr)) {
        throw new TypeError('The argument should be an array! ');
    };
    return new MyPromise(function(resolve, reject) {
        var i = 0, result = [];
        next();
        function next() {
            // Convert those that are not MyPromise instances
            MyPromise.resolve(arr[i]).then(function (res) {
                result.push(res);
                i++;
                if (i === arr.length) {
                    resolve(result);
                } else{ next(); }; }, reject); }})}; MyPromise.race =function(arr) {
    if (!Array.isArray(arr)) {
        throw new TypeError('The argument should be an array! ');
    };
    return new MyPromise(function(resolve, reject) {
        let done = false;
        arr.forEach(function(item) {
            MyPromise.resolve(item).then(function (res) {
                if(! done) { resolve(res); done =true;
                };
            }, function(rej) {
                if(! done) { reject(rej); 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.

MyPromise.prototype.catch = function(errHandler) {
    return this.then(undefined, errHandler);
};
MyPromise.prototype.finally = function(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.
function MyPromise(callback) {
    this.status = PENDING; // Store state
    this.__succ__res = null; // Save the resolve result
    this.__err__res = null; // save the reject result
    this.__queue = []; // Event queue

    var _this = this;
    function resolver(res) {
        _this.status = FULFILLED;
        _this.__succ__res = res;
        _this.__queue.forEach(item= > {
            item.resolve(res);
        });
    };
    function rejecter(rej) {
        _this.status = REJECTED;
        _this.__err__res = rej;
        _this.__queue.forEach(item= > {
            item.reject(rej);
        });
    };
    try {   / / - + in the try... The catch... To run the callback function
        callback(resolver, rejecter);
    } catch (err) {
        this.__err__res = err;
        this.status = REJECTED;
        this.__queue.forEach(function(item) {
            item.reject(err);
        });
    };
};

MyPromise.prototype.then = function(onFulfilled, onRejected) {
    var _this = this;
    return new MyPromise(function(resFn, rejFn) {
        if (_this.status === FULFILLED) {
            handleFulfilled(_this.__succ__res);
        } else if (_this.status === REJECTED) {
            handleRejected(_this.__err__res);
        } else {/ / pending state
            _this.__queue.push({resolve: handleFulfilled, reject: handleRejected});
        };

        function handleFulfilled(value) {
            var returnVal = value;
            // Retrieves the return result of the ondepressing function
            if (onFulfilled instanceof Function) {
                try {       / / - + in the try... The catch... Run the ondepressing callback function in
                    returnVal = onFulfilled(value);
                } catch (err) { // Code error handling
                    rejFn(err);
                    return;
                };
            };

            if (returnVal && returnVal['then'] instanceof Function) {
                returnVal.then(function(res) {
                    resFn(res);
                },function(rej) {
                    rejFn(rej);
                });
            } else {
                resFn(returnVal);
            };
        };
        function handleRejected(reason) {
            if (onRejected instanceof Function) {
                var returnVal
                try {/ / - + in the try... The catch... To run the onRejected callback function
                    returnVal = onRejected(reason);
                } catch (err) {
                    rejFn(err);
                    return;
                };
                if (typeofreturnVal ! = ='undefined' && returnVal['then'] instanceof Function) {
                    returnVal.then(function(res) {
                        resFn(res);
                    },function(rej) {
                        rejFn(rej);
                    });
                } else {
                    resFn(returnVal);
                };
            } else {
                rejFn(reason)
            }
        }

    })
};
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.

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

    var _this = this;
    function resolver(res) {
        if (_this.status === PENDING) {
            _this.status = FULFILLED;
            _this.__succ__res = res;
            _this.__queue.forEach(item= > {
                item.resolve(res);
            });
        };
    };
    function rejecter(rej) {
        if (_this.status === PENDING) {
            _this.status = REJECTED;
            _this.__err__res = rej;
            _this.__queue.forEach(item= > {
                item.reject(rej);
            });
        };
    };
    
    / / a little...
};
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
  • Promises/A + specification
  • Detailed explanation of JavaScript operation mechanism: Talk about Event Loop again