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 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
- 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.js
Play while looking at code; - Same code, up here
MyPromise
The following is the nativePromise
The 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 that
Promise
First 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/catch
The 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.
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
First, define the MyPromise class
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 itselfresolve
和reject
, 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, 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
. arg
And pass in parameters. this.__succ_res
Both use extended operators that are passed unchanged to handle multiple argumentsthen
Method 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,
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.
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:
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
handle
Function, which takes the result of success as an argument; handle
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
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 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 = (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:
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.
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