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 implemented
Promise
Basic functions, like the original, asynchronous and synchronous operations are ok, including:MyPromise.prototype.then()
MyPromise.prototype.catch()
With nativePromise
Slight discrepancyMyPromise.prototype.finally()
MyPromise.all()
MyPromise.race()
MyPromise.resolve()
MyPromise.reject()
rejected
The bubbling of the state has also been resolved, with the current Promise’s Reject bubbling until the end, until the catch, if not caughtMyPromise
Once 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 custom
console.mylog()
The method is used to output the result, with the first argument being the one currently in usePromise
Object, 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.js
Play while looking at code; - Same code, up here
MyPromise
The following is the nativePromise
The result of operation;
harvest
- Write again and have new harvest, write more smoothly, understand more deeply;
then/catch
The method is the most difficult, tinkering;reject
Bubbling 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 function
callback
. When a newMyPromise
Object, we need to run this callback, andcallback
It also has two parameters of itselfresolver
和rejecter
, 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 function
callback
If yesresolve
State to save the result inthis.__succ_res
, the status is marked as success; If it isreject
State, operation similar; - It also defines the most commonly used
then
Method, is a prototype method; - perform
then
Method 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,
then
Methods in thepending
State, so add oneelse
; - perform
else
“, 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.__queue
Save the event queue; - When the asynchronous code has finished executing, this time the
this.__queue
All callbacks in the queue are executed, if yesresolve
If the command is in the state, run the corresponding commandresolve
The 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:
MyPromise
Object;- with
then
Method object; - Other values. Each of these three cases is treated separately.
- The first one is,
then
Method returns aMyPromise
Object received by its callback functionresFn
andrejFn
Two callback functions; - Encapsulate the handling code for the success status as
handleFulfilled
Function, which takes the result of success as an argument; handleFulfilled
In the function, according toonFulfilled
Returns different values, do different processing:- First, get
onFulfilled
The return value (if any) of thereturnVal
; - Then, judge
returnVal
Is there a then method that includes cases 1 and 2 discussed above (it isMyPromise
Object, or hasthen
Other objects of a method) are the same to us; - And then, if there are
then
Method, which is called immediatelythen
Method, throw the results of success and failure to the newMyPromise
Object callback function; No result is passedresFn
Callback function.
- First, get
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 a
MyPromise
Instance; - The parameter is a
thenable
Object; - Parameter does not have
then
Method 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 is
MyPromise
Instance, no action is required; - And then, the parameters are something else
thenable
Object, which is calledthen
Method to pass the corresponding value to newMyPromise
Object 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:
MyPromise
Object constructor callbackthen
Method’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