You smiled and talked to me of nothing. And I feel that for this I have been waiting long
preface
After reading the first three chapters of the second part of “JavaScript you Don’t Know” (Middle volume) + Ruan Yifeng’s Introduction to ECMAScript 6, Section 16 of Promise objects, my understanding of Promise improved to a higher level.
In the accidental brush to 45 Promise interview questions A cool in the end of this article, in line with the psychological test of learning results, patience to finish, the author can be said to be really careful, this article finally interview questions are really good, but according to the Promise /A+ to achieve A Promise of their own this question, should be the most difficult, Also often take an examination of the interview question, of course, the interview only requires a brief answer, the online article is sameness, also do not know who copy who. I can’t say that I can write anything new, but if I don’t do it manually, I will never be able to overcome the obstacles in my heart. If my eyes are good, my hands are also good. After all, practice is the only criterion for testing truth.
In this post, I’m going to do a summary introduction to promises based on what I know so far, which also expands on some of the knowledge involved.
Why is it neededPromise
?
Every thing has its existence meaning, so we need to know why Promise appears and what problems it solves.
The callback nesting
Before promises were born, we used callbacks to express program asynchrony and manage concurrency, although some older projects are still used to ensure compatibility. Callbacks are the simplest way to implement asynchrony in JavaScript, and you can think of a callback as a continuation of a program that will be executed at some future time after the current synchronized code has finished executing. Nesting occurs when our callback needs the result of the previous callback, something like this:
ajax1(null.function (value1) {
ajax2(value1, function(value2) {
ajax3(value2, function(value3) {
ajax4(value3, function(value4) {
// Do something with value4
});
});
});
});
Copy the code
With more layers of nesting, you get what’s called callback hell, also known as the pyramid of destruction. But it’s a lot more complicated than that, making it exponentially harder to track the order of code execution. You must have also encountered less}) at the end of the situation, the inspection of a lot of time, this is because the code is too complex.
The problems presented by the callback hell can be summarized as follows:
- It is difficult to track the order in which code is executed
- The code is complex and extremely fragile
- If the program is abnormal and the execution sequence is out of order, the result is hard to predict (the previous callback will block the execution of the following code, if the previous failure, the following will not be executed).
Trust issues
We often use third-party apis (such as Ajax), and when we deliver a callback to a third party, we transfer control to that third party, which is called inversion of control. Our callbacks are called by a third party, but can we trust them enough?
Here are some examples:
- Calling the callback too early
- Calling the callback too late
- Too many callbacks were called
- The callback is not called
- Failed to pass parameter/environment value
- Swallow possible errors or exceptions
In each of these cases, we might have to do some processing in the callback function, and we might have to do the same processing every time the callback function is executed, which leads to a lot of duplicate code.
So Promise came along to address these two major problems.
Promise
How to solve the above problems?
The callback nesting
As far as callbacks go, Promise simply changes the location of the callbacks to make the code look more sequential.
Then methods are defined on the prototype chain of the Promise object, and calling then methods automatically creates a new Promise to return from the call, thereby enabling chained invocation. The purpose of chain calls is to get things to execute sequentially, to solve the nesting of callbacks, in a way that’s more in line with the way our human brains think sequentially. In the example above, we used Promise as follows:
/ / request method
function request(param){
return new Promise(function(resolve, reject){
ajax(param, function(val){
resolve(val)
})
})
}
// chain calls are executed sequentially (1,2,3,4 to indicate the order)
request1(null)
.then(value1= > request2(value1))
.then(value2= > request3(value2))
.then(value3= > request4(value3))
Copy the code
Trust issues
One of the biggest causes of trust problems is that we hand over control of callback to a third party. Would we have allowed it to happen if control had been in our hands?
And the answer is absolutely not, so essentially what Promise does is it reverses control one more time, reverses control one more time, reverses control two more times, gets control back to us. Ajax2, ajax3, ajax3, ajax2, ajax3, ajax3, ajax2, ajax3, ajax3 The resolve method is used instead. The idea is that when the third party method completes, it will send you a notification notifying you to perform the next step. Of course, the notification contains all the information you need to perform the next step. Perfect
So, Promise doesn’t get rid of callbacks completely, it just changes where they’re delivered. Look at the questions listed above and what you know about promises. Do they all work out?
-
Calling the callback too early
✅ provided to then(..) The callback is always called asynchronously (microqueue), there is no call too early, this problem solved
-
Calling the callback too late
✅ can be sure that the resolution Promise then(..) The registered observation callback must be triggered at the next asynchronous event point. This problem is resolved
-
Too many callbacks were called
✅Promise is defined in such a way that it can only be resolved once, so it will only be called once. This problem is resolved
-
The callback is not called
✅ Promises always invoke either a complete callback or a reject callback when they are resolved. If the Promise itself is never resolved, even then, promises offer a solution:
const p = Promise.race([ fetch('/resource-that-may-take-a-while'), new Promise(function (resolve, reject) { setTimeout(() = > reject(new Error('request timeout')), 5000)})]); p .then(val= > console.log(val)) // Finish in time .catch(err= > console.error(err)); / / timeout Copy the code
-
Failed to pass parameter/environment value
✅ If you do not specify any value, the value is undefined, problem solved
-
Swallow possible errors or exceptions
✅ can catch an exception using the second argument to then or the catch method, and the problem is resolved
The Promise feature is specifically designed to provide a valid reusable answer to the trust question encoded by the callback. It eliminates the need to repeat the same process every time a callback is executed in order to prevent the above unexpected situations.
Promise
specification
Promise was first introduced in the CommonJS community with a number of specifications. The more accepted is the Promise /A specification. However, the Promise /A specification is relatively simple. Later, people proposed the Promise /A+ specification on this basis, which is actually the specification implemented in the industry. ES6 also uses this specification, but ES6 also adds promise. all, promise. race, promise. catch, promise. resolve, promise. reject and other methods to this specification.
The promise/A+ specification is as follows:
-
There is only one then method, no catch, race, all, etc., not even a constructor
The Promise standard only specifies the behavior of the then method of a Promise object. All other common methods/functions are not specified, including catch, race, all and other common methods. It does not even specify how to construct a Promise object
-
The then method returns a new Promise
-
Different Promise implementations need to be interoperable
-
The initial state of a Promise is pending, from which the state can be changed to depressing or Rejected. Once the state is determined, it cannot be changed to other states again. The process of state determination is called settle
-
More specific standards are shown here in English and Chinese
Promise
implementation
The constructor
Promise is essentially a state machine composed of three states, pending, depressing and Rejected. The initial state is pending. For an explanation of the state machine, see here 👉👉👉, and you can see that Promise fits the definition pretty well.
Therefore, we define constants to hold the three states, with state representing the current state. Since each Promise instance generated by calling the constructor has its own state, this refers to the instance under the new binding principle, so we mount state on this. Now the code looks like this:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
this.state = PENDING;
}
Copy the code
When we call the Promise constructor with the new operator, the constructor immediately calls the passed executor, which in turn takes resolve and reject, both of function type.
One more detail: why is the first argument to executor always named resolve instead of fulfill?
🙋 : resolve, fulfill, reject. According to common sense, the first parameter should be named fulfill. But the first parameter callback in the definition is usually used to indicate that a Promise has been completed, which in this case means either completed or rejected. A little convoluted, for example promise.resolve (..) Is expanded for thenable passed in. If the thenable expansion results in a rejection state, then from promise.resolve (..) The returned Promise is essentially a rejection state. Also Promise (..) The constructor’s first argument callback also expands thenable (and promise.resolve (..)) Same as) or a real Promise, so the resolve name is precise.
At this point our code looks like this:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
this.state = PENDING;
function resolve(value){
// TODO
}
function reject(reason){
// TODO
}
executor(resolve, reject);
}
Copy the code
On the other hand, executors can make mistakes, so we wrap them in a try/catch block, reject the exception we catch.
Why reject rather than throw? Consider that the second argument to then is used to handle the previous rejection.
Now our code looks like this:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
this.state = PENDING;
function resolve(value){
// TODO
}
function reject(reason){
// TODO
}
try{
executor(resolve, reject);
}catch(error){ reject(error); }}Copy the code
The next step is to implement the resolve and reject functions. These two functions change the state of the current PROMISE instance. Note that this can only be changed if the current state is pending. Note that we cannot write this in resolve or Reject, where this refers to the Window object. Strict mode points to undefined. We need this to refer to the instance, so we also need this to refer to the instance. The comprehensive code is as follows:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
const _this = this;
_this.state = PENDING;
function resolve(value){
if(_this.state === PENDING){ _this.state = FULFILLED; }}function reject(reason){
if(_this.state === PENDING){ _this.state = REJECTED; }}try{
executor(resolve, reject);
}catch(error){ reject(error); }}Copy the code
The constructor is not complete, so let’s think about that, implement the THEN method, and come back to supplement the constructor as we go.
then
methods
When the state of the Promise instance changes, the then callback is triggered, whether it succeeds or fails. It can be called chained, which is of course the method mounted on the prototype, and its two arguments are functions that receive the result of success and the cause of failure, respectively.
So here’s the problem, how do we get the state value of the Promise instance, and how do we get the success or failure result?
🙋 : is the then method called by the Promise instance. According to the implicit binding principle, this inside the THEN method refers to the Promise instance, so we get the state value directly from this. For the successful result or the cause of the failure, naturally we also have to take from this.
So we modify the constructor as follows:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
const _this = this;
_this.state = PENDING;
_this.value = void 0;
_this.reason = void 0;
function resolve(value){
if(_this.state === PENDING){ _this.state = FULFILLED; _this.value = value; }}function reject(reason){
if(_this.state === PENDING){ _this.state = REJECTED; _this.reason = reason; }}try{
executor(resolve, reject);
}catch(error){ reject(error); }}Copy the code
Notice that value and Reason are initialized with void 0. Why?
🙋 : undefined is not just one of the basic types, it is also a variable, not a keyword.
Void can be followed by any expression, and undefined is returned after the expression is evaluated.
The reason for using void 0 instead of undefined, I think, is simply that it’s safer and saves little memory. For a more detailed explanation, I quote directly from the web:
- Prior to ES5, undefined could be overridden in Windows, which caused some errors in extreme cases. Void 0 is used to prevent undefined from being overwritten.
- You can reduce the bytes. Void 0 saves 3 bytes instead of undefined.
Note: in the standard after ES5, it is stipulated that the value of undefined in global variables is read-only and cannot be overwritten, but it can still be overwritten in local variables. Note: Undefined can be overwritten in non-strict mode, but not in strict mode.
The implementation of then is as follows:
Promise.prototype.then = function(onFulfilled, onRejected){
if(this.state === FULFILLED){
typeof onFulfilled === 'function' && onFulfilled(this.value);
}
if(this.state === REJECTED){
typeof onRejected === 'function' && onRejected(this.reason);
}
if(this.state === PENDING){
// TODO}}Copy the code
OnFulfilled and onRejected are also judged by the type judgment, because the specification is as follows:
OnFulfilled and onRejected are both optional parameters. If ondepressing is not a function, it must be ignored. If onRejected is not a function, it must be ignored.
This is a big pity. If onFulfilled is not a function, then a default onFulfilled function will come up, which is the essence of the Promise value:
var p = Promise.resolve( 42 );
p.then(
// If any non-function values are omitted or passed
// function onFulfilled(v) {
// return v;
// }
null.function onRejected(err){
// It will never get here})Copy the code
Similarly, if onRejected is not a function, a default onRejected function will be replaced by the catch method:
var p = Promise.reject( 42 );
var p2 = p.then(
function onFulfilled(){
// Never get here
}
// Reject the assumed handler if any non-function values are omitted or passed in
// function onRejected(err) {
// throw err;
// }
)
Copy the code
So change the then method as follows:
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };
if(this.state === FULFILLED){
onFulfilled(this.value);
}
if(this.state === REJECTED){
onRejected(this.reason);
}
if(this.state === PENDING){
// TODO}}Copy the code
The specification states that the then method returns a new Promise, so we cannot return this (the Promise instance generated by the new call). Then modify as follows:
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };
const promise2 = new Promise((resolve, reject) = > {
if(this.state === FULFILLED){
let x = onFulfilled(this.value);
resolve(x);
}
if(this.state === REJECTED){
let x = onRejected(this.reason);
reject(x);
}
if(this.state === PENDING){
// TODO}})return promise2;
}
Copy the code
Because onFulfilled and onRejected may fail, we will add a try/catch block. What if the x value above is a Promise object? Quite simply, we can call the then method with x, recursively, until the value of x is not a Promise object. Wait, before you change the code, the spec says something like this:
If ondepressing or onRejected returns a value x, run the following Promise: [[Resolve]](promise2, x)
The Promise resolution process is an abstract operation that takes in a Promise and a value, plus we need to process it once we get the x value, so we pass in four parameters altogether. We’ll call this resolution a resolvePromise, which we’ll first abstract out and then handle the case we’ve just considered and other cases in it, so change it as follows:
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };
const promise2 = new Promise((resolve, reject) = > {
if(this.state === FULFILLED){
try{
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }}if(this.state === REJECTED){
try{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }}if(this.state === PENDING){
// TODO}})return promise2;
}
Copy the code
So I’m going to write this code here, but notice that in the THEN method I’m always leaving pending state unhandled. This will be a big pity or rejected. I will surely forget my promise decision, which is really unnecessary. Imagine that the promise has no resolution at all or is delayed for some reason. In both cases, the then method is called and the state value obtained within the method must be pending. This is a big pity, but then has been called. What should we do now? After the future state value changes, we will perform the onFulfilled and onRejected methods we passed in before.
🙋 : When judging the status value as pending, we can save the ondepressing and onRejected methods respectively, and then call the resolve or reject methods in the constructor. Therefore, we need to define two variables in the constructor and on the instance to store them, and consider that multiple THEN methods can be registered on a Promise instance (i.e. P. Cohen (onFulfilled, onRejected); P.t hen (onFulfilled, onRejected);) In order to avoid saving the later overwrite the previous one when the onFulfilled and onRejected methods are saved, the variables stored here need to be defined as an array. Modify the constructor as follows:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
const _this = this;
_this.state = PENDING;
_this.value = void 0;
_this.reason = void 0;
_this.onResolvedCallback = [];
_this.onRejectedCallback = [];
function resolve(value){
if(_this.state === PENDING){
_this.state = FULFILLED;
_this.value = value;
_this.onResolvedCallback.length > 0 &&
_this.onResolvedCallback.forEach(fn= >fn()); }}function reject(reason){
if(_this.state === PENDING){
_this.state = REJECTED;
_this.reason = reason;
_this.onRejectedCallback.length > 0 &&
_this.onRejectedCallback.forEach(fn= >fn()); }}try{
executor(resolve, reject);
}catch(error){ reject(error); }}Copy the code
perfect! The constructors are all set. As mentioned above, the then method also needs to be modified. Considering that the THEN method is called asynchronously, we will call the THEN method directly, which must be executed synchronously. In order to achieve asynchrony, we will use setTimeout function first, which belongs to macro task, of course. MutationObserver should be used to generate microtasks in the browser, which we will implement later. It was amended as follows:
Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };
const promise2 = new Promise((resolve, reject) = > {
if(this.state === FULFILLED){
setTimeout(() = >{
try{
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}if(this.state === REJECTED){
setTimeout(() = >{
try{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}if(this.state === PENDING){
this.onResolvedCallback.push(() = > {
setTimeout(() = >{
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})});this.onRejectedCallback.push(() = > {
setTimeout(() = >{
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}); }})return promise2;
}
Copy the code
Notice that I’ve been using the arrow function above, so there’s no problem with this pointing to change.
The second optional parameter of the setTimeout function above is not specified. The default is 0, but is the delay really 0ms?
Some people say that the minimum delay of setTimeout is 4ms, but this is preconditioned, so it is not accurate. Chrome, for example, has a minimum latency of 1ms. If timer nesting level >=5, then the minimum delay is 4ms. Take a look at the following chrome test code and you’ll see:
setTimeout(() = >{console.log(5)},5)
setTimeout(() = >{console.log(4)},4)
setTimeout(() = >{console.log(3)},3)
setTimeout(() = >{console.log(2)},2)
setTimeout(() = >{console.log(1)},1)
setTimeout(() = >{console.log(0)},0)
// Result: 1 0 2 3 4 5
setTimeout(() = > {
setTimeout(() = > {
setTimeout(() = > {
setTimeout(() = > {
setTimeout(() = > {
// This is the timer nested hierarchy
}, 0)},0)},0)},0)},0)
Copy the code
You can read more about the delay here 👉👉👉
Well, all that’s missing is the fulfillment of the Promise resolution process. Because the Promise was first implemented by the community and then gradually formed into a specification, many of the early Promise libraries were not entirely consistent with the specification. For this, in order to accommodate those early implementations that do not fully conform to the specification, the specification clearly defines the handling of various cases, so we do not have to take pains to consider various cases, they are listed, we control the step by step implementation is good. Let’s start implementing the resolvePromise function.
Promise
To solve the process
-
X is equal to promise
If a promise and x point to the same object, reject the promise as TypeError grounds
function resolvePromise(promise2, x, resolve, reject){
if(x === promise2) return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
}
Copy the code
Var p2 = p.chen (() => p2);
-
X to Promise
If x is a Promise, make the Promise accept the state of x:
- if
x
In the waiting state,promise
Must remain in wait state untilx
To be carried out or rejected - if
x
In the execution state, execute with the same valuepromise
- if
x
In the rejection state, reject with the same groundspromise
- if
This can be skipped directly, because anything that has then(..) Both method objects and functions (that is, thenable) are recognized as Promise objects. Instanceof is not sufficient as a check, considering that libraries or frameworks may choose to implement their own promises rather than using the native ES6 Promise implementation. Just like what we’re doing now, we define a Promise function, then we add the then method to the prototype, and we think of it as a Peromise object.
So this is the case where x is thenable down here, or you can see that this is the case where x is Promise. At this point, I’m not going to bother here, and it turns out I was right, because the rest of the test passed perfectly. (Less work than more work ~ ~ ~)
-
X is an object or function
- the
x.then
Assigned tothen
- If you take
x.then
Throws an error when the value ofe
, thee
Refusal on grounds of proofpromise
- if
then
Theta is a function of thetax
As the scope of the functionthis
Call it. Pass two callback functions as arguments, the first of which is calledresolvePromise
The second parameter is calledrejectPromise
:- if
resolvePromise
In order to valuey
Run when called for parameters[[Resolve]](promise, y)
- if
rejectPromise
To according to ther
Is called with an argument, then with an argumentr
Refused topromise
- if
resolvePromise
和rejectPromise
Are called, or are called more than once by the same parameter, the first call takes precedence and the rest are ignored - If the call
then
Method throws an exceptione
:- if
resolvePromise
或rejectPromise
Already called, ignore it - Or otherwise
e
Refusal on grounds of proofpromise
- if
- if
then
It’s not a functionx
Execute for parameterspromise
- if
- if
x
Is not an object or a functionx
Execute for parameterspromise
function resolvePromise(promise2, x, resolve, reject){ if(x === promise2) return reject(new TypeError('Chaining cycle detected for promise #<Promise>')); if(x && typeof x === 'object' || typeof x === 'function') {let called = false; // To check whether it is called try{ let then = x.then; // This line must be in a try/catch block if(typeof then === 'function'){ then.call(x, y= > { if(called) return; called = true; resolvePromise(promise2, y, resolve, reject); // This is recursive processing }, r= > { if(called) return; called = true; reject(r); }); }else{ resolve(x); }}catch(e){ if(called) return; called = true; reject(e); }}else{ resolve(x); }}Copy the code
If x is null, undefined is not allowed. If x is null, undefined is not allowed. If x is null, undefined is allowed.
🙋 : Consider extreme cases under accessor properties:
let x = { get then() {throw 'wrong'; } } x.then; / / Uncaught error Copy the code
So when you get x. teng, make sure you put it in a try/catch block.
- the
When Nice is completed, we can see that there is only one difficulty: the Promise solution process, but it is not difficult to implement against the specification. Now that we have the Promise method in place, let’s implement some of its extensions and test it out.
Promise.prototype.catch()
The first argument to the then method is set to NULL, as long as it is not a function.
Promise.prototype.catch = function (onRejected) {
return this.then(null, onRejected);
};
Copy the code
Promise.prototype.finally()
The finally() method is used to specify actions that will be performed regardless of the final state of the Promise object. Write the same statement once for both success and failure. The finally method’s callback takes no arguments because it does not depend on the result of the Promise’s execution.
Promise.prototype.finally = function (callback) {
let P = this.constructor; // P points to the constructor for the current PROMISE instance, which is the Promise constructor
return this.then(
value= > P.resolve(callback()).then(() = > value),
reason= > P.resolve(callback()).then(() = > { throw reason })
);
};
Copy the code
Promise.resolve()
Resolve (); resolve(); resolve(); resolve(); Consider the case where value, if of type promise, is returned without processing.
Promise.resolve = function(value) {
if(value instanceof Promise) return value
const promise = new Promise(function(resolve, reject) {
resolvePromise(promise, value, resolve, reject)
});
return promise;
}
Copy the code
Promise.reject()
There’s nothing to be said for returning a rejected Promise instance.
Promise.reject = function(reason) {
return new Promise(function(resolve, reject) {
reject(reason)
});
}
Copy the code
Promise.all()
The promise.all () method is used to wrap multiple Promise instances into a new Promise instance. The states of all instances will become a big pity, and the states of newly generated instances will become a big pity. When an instance is rejected, the state of the newly generated instance becomes Rejected.
Note that:
fulfilled
The return value under state is an array that stores the return value of each instance in order, whilerejected
State is the return value of the first rejected instancePromise.all()
The arguments to a method may not be arrays, but they must beIterator
Interface, and each member returned is a Promise instance
Promise.all = function(可迭代){
// Assuming that the parameters passed are objects with an Iterator interface, such as arrays or strings, too much validation is omitted
const results = []; // Store the result
let index = 0; // Record the subscript
let count = 0; // Record the number of stored results
return new Promise(function(resolve, reject){
if(iterable[Symbol.iterator]().next().done) // Determine if the iterable passed is empty
return resolve([]); / / return to terminate
for(let item of iterable){
let ind = index++; // as long as the loop subscript +1, use let to store a unique subscript per loop
Promise.resolve(item) // The iterated value, whether of a promise type or a primitive type, is declared as a promise
.then(value= >{
results[ind] = value;
count++; // Save the result before +1
if(count === iterable.length) // If the length is the same, the criteria are complete
resolve(results);
}, reason= > {
reject(reason); // // If there is a single failure, the return promise state is Reject}}})); }Copy the code
Considerations for the above technical details:
-
Resolve () is the best and safest way to encapsulate a Thenable object in a loop
-
for… The of loop is used to iterate over an Iterator object. Many people use the original for loop to iterate over an Iterator object without considering the object that defines the Iterator interface
-
for… The of loop does not support subscripts, and to ensure the order in which data is returned, we need to assign values using subscripts instead of push().
-
Arrays are assigned by subscripts, but there’s one thing to be careful about:
var a = []; a[3] = 3; a.length; / / 4 a; // [empty × 3, 3 Copy the code
The length of the array is not enough to indicate the number of results, so I define a count variable to record
-
To determine that the iterable passed in is empty, I consider calling its iterator interface to generate the iterator, and then calling next() for the first time produces an object whose done property is true and the iterable must be empty. Empty iterable, for… “Of” is not going to work
To prove my last consideration:
// The empty String "and the empty Array [] are empty iterables because String and Array deploy the Symbol. Iterator interface
' '[Symbol.iterator]().next().done; // true[] [Symbol.iterator]().next().done; // true
// I will directly resolve([]) because of the following test
Promise.all(' ').then(val= >{console.log(val)}); / / []
Promise.all([]).then(val= >{console.log(val)}); / / []
Copy the code
Of course, the promise.all () specification is very detailed on MDN, so I won’t worry about that much detail here, but just implement it.
Promise.race()
The promise.race () method again wraps multiple Promise instances into a new Promise instance. Whenever one instance changes state first, the state of the newly generated instance changes with it. The return value of the first changed Promise instance is passed to the callback function of the newly generated instance.
Promise.race = function(可迭代){
// Assuming that the parameters passed are objects with an Iterator interface, such as arrays or strings, too much validation is omitted
return new Promise(function(resolve, reject){
for(let item of iterable){
Promise.resolve(item)
.then(value= > {
resolve(value);
}, reason= >{ reject(reason); })}})}Copy the code
Promise.race() is simpler to implement than promise.all (). There is no judgment that the iterable passed in is empty, because this is particularly important:
If toPromise.all([ .. ] )
Pass in an empty array, it’ll do immediately, butPromise.race([ .. ] )
Will hang, and will never be resolved.
Because it is never resolved, there is no need to deal with it. It is also impossible for “of” to do anything on an empty iterable.
Promise.allSettled()
The promise.allSettled () method also takes a set of Promise instances as parameters and wraps them into a new Promise instance. The wrapper instance will not complete until all of these parameter instances return the result, whether this is fulfilled or Rejected.
This method returns a new Promise instance. Once it is fulfilled, the state is always fulfilled and will not become Rejected. After the state becomes depressing, the Promise listener receives an array of parameters, each member corresponding to an incoming promise.allSettled () Promise instance.
For each result object, there is a status string. If its value is fulfilled, there is a value on the result object. If the value is rejected, there is a reason. Value (or Reason) reflects the value of each promise resolution (or rejection).
Promise.allSettled = function(可迭代){
// Assuming that the parameters passed are objects with an Iterator interface, such as arrays or strings, too much validation is omitted
const results = []; // Store the result
let index = 0; // Record the subscript
let count = 0; // Record the number of stored results
return new Promise(function(resolve, reject){
if(iterable[Symbol.iterator]().next().done) // Determine if the iterable passed is empty
return resolve([]); / / return to terminate
for(let item of iterable){
let ind = index++; // as long as the loop subscript +1, use let to store a unique subscript per loop
Promise.resolve(item) // The iterated value, whether of a promise type or a primitive type, is declared as a promise
.then(value= >{
results[ind] = { status: 'fulfilled', value };
count++;
if(count === iterable.length) // If the length is the same, the criteria are complete
resolve(results);
}, reason= > {
results[ind] = { status: 'rejected', reason };
count++;
if(count === iterable.length) // If the length is the same, the criteria are complete
resolve(results); // Because the state of the packing instance will only become depressing}}})); }Copy the code
It’s also convenient to change from promise.all ().
Promise.any()
The promise.any () method takes a set of Promise instances as parameters and wraps them into a new Promise instance. As long as one parameter instance becomes a depressing state, the packaging instance will become a depressing state. If all parameter instances become the Rejected state, the wrapper instance becomes the Rejected state.
Note that the error thrown by promise.any () is not a generic error, but an AggregateError instance. It is equivalent to an array, with each member corresponding to an error thrown by the Rejected operation.
new AggregateError() extends Array -> AggregateError
const err = new AggregateError();
err.push(new Error("first error"));
err.push(new Error("second error"));
throw err;
Copy the code
Although promise.any () is still experimental, with the above ideas in mind, let’s simply implement it:
Promise.any = function(可迭代){
// Assuming that the parameters passed are objects with an Iterator interface, such as arrays or strings, too much validation is omitted
const err = new AggregateError(); // Used to store errors
let index = 0; // Record the subscript
let count = 0; // Record the number of stored results
return new Promise(function(resolve, reject){
if(iterable[Symbol.iterator]().next().done) // Determine if the iterable passed is empty
return resolve([]); / / return to terminate
for(let item of iterable){
let ind = index++; // as long as the loop subscript +1, use let to store a unique subscript per loop
Promise.resolve(item)
.then(value= > {
resolve(value);
}, reason= > {
err[ind] = (new Error(reason));
count++;
if(count === iterable.length) // If the length is the same, the criteria are completereject(err); })}})}Copy the code
If the argument passed in is an empty iterable, this method will return a completed promise synchronously, so I added the judgment for an empty iterable. Since AggregateError inherits from Array, it must have the same properties as Array.
Promise
test
This completes the Promise implementation and the commonly used extension API, so let’s test it here. Following the usual practice on the web, first post the overall code:
const PENDING = 'pending';
const FULFILLED = 'fulfilled';
const REJECTED = 'rejected';
function Promise(executor){
const _this = this;
_this.state = PENDING;
_this.value = void 0;
_this.reason = void 0;
_this.onResolvedCallback = [];
_this.onRejectedCallback = [];
function resolve(value){
if(_this.state === PENDING){
_this.state = FULFILLED;
_this.value = value;
_this.onResolvedCallback.length > 0 &&
_this.onResolvedCallback.forEach(fn= >fn()); }}function reject(reason){
if(_this.state === PENDING){
_this.state = REJECTED;
_this.reason = reason;
_this.onRejectedCallback.length > 0 &&
_this.onRejectedCallback.forEach(fn= >fn()); }}try{
executor(resolve, reject);
}catch(error){ reject(error); }}function resolvePromise(promise2, x, resolve, reject){
if(x === promise2)
return reject(new TypeError('Chaining cycle detected for promise #<Promise>'));
if(x && typeof x === 'object' || typeof x === 'function') {let called = false; // To check whether it is called
try{
let then = x.then;
if(typeof then === 'function'){
then.call(x, y= > {
if(called) return;
called = true;
resolvePromise(promise2, y, resolve, reject); // This is recursive processing
}, r= > {
if(called) return;
called = true;
reject(r);
});
}else{ resolve(x); }}catch(e){
if(called) return;
called = true; reject(e); }}else{ resolve(x); }}Promise.prototype.then = function(onFulfilled, onRejected){
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function' ? onRejected : reason= > { throw reason };
const promise2 = new Promise((resolve, reject) = > {
if(this.state === FULFILLED){
setTimeout(() = >{
try{
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}if(this.state === REJECTED){
setTimeout(() = >{
try{
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}if(this.state === PENDING){
this.onResolvedCallback.push(() = > {
setTimeout(() = >{
try {
let x = onFulfilled(this.value);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})});this.onRejectedCallback.push(() = > {
setTimeout(() = >{
try {
let x = onRejected(this.reason);
resolvePromise(promise2, x, resolve, reject);
}catch(error){ reject(error); }})}); }})return promise2;
}
Copy the code
Add our promise.resolve () and promise.reject () methods to the bottom of the code, and it doesn’t really matter. Then add some additional code for the test. The total is as follows:
Promise.resolve = function(value) {
if(value instanceof Promise) return value
var promise = new Promise(function(fulfill, reject) {
resolvePromise(promise, value, fulfill, reject)
})
return promise
}
Promise.reject = function(reason) {
return new Promise(function(fulfill, reject) {
reject(reason)
})
}
Promise.deferred = Promise.defer = function() {
var dfd = {}
dfd.promise = new Promise(function(fulfill, reject) {
dfd.resolve = fulfill
dfd.reject = reject
})
return dfd
}
module.exports = Promise
Copy the code
Then take two steps:
-
Promises -aplus-tests install: NPM install -g Promises -aplus-tests
-
Go to the directory where you saved the file (I’m promise.js) and run: promises-aplus-tests promise.js
When we see 872 passing, the test passes!!
For the extension method of the test, I still want to write a few samples, self-test, to avoid mistakes!!
The function version of the promise has been implemented, the class version of the same, a little change, pay attention to this, I will not be verbose here.
throughMutationObserver
Achieve true meaningPromise
There are only a handful of microtasks: process. nextTick (Node only), Promise, Object.observe(deprecated), MutationObserver
We used the setTimeout function above to achieve asynasynism, but this is a macro task, Promise is a microtask, and currently only MutationObserver can generate a microtask in the browser. For MutationObserver, check out MDN.
Why must microtasks be used?
🙋 :
SetTimeout a single delay of 4ms might not be a problem in a typical Web application, but consider the extreme case where we have 20 Promise chained calls and the code run time, then the first and last line of the chained call will probably run over 100ms. If there is no UPDATE to the UI in between, there is no performance problem per se, but there may be some stuttering or flickering, which is not common in Web applications, but can happen in Node applications. So an implementation that can be used in a production environment needs to eliminate this delay. In Node, we can call process.nexttick. In summary, we need to implement a function that behaves like setTimeout, but asynchronously and as soon as possible to call all queued functions.
Here is a microtask nextTick function implemented using MutationObserver:
function nextTick(fn) {
if(process ! = =undefined && typeof process.nextTick === 'function')
return process.nextTick(fn)
else {
var counter = 1
var observer = new MutationObserver(fn)
var textNode = document.createTextNode(String(counter))
observer.observe(textNode, {
characterData: true
})
counter = counter+1
textNode.data = String(counter)
}
}
Copy the code
We simply put the above function into the implemented code and replace setTimeout with nextTick in the code. Of course, the test also passed, but this was done in the Node environment, using the microtask generated by process.nextTick in the nextTick function above. I don’t know about the browser test.
aboutPromise
Practical application of
When it comes to practical applications, I think it’s all about the business needs of your job. Of course, to 45 Promise interview questions a cool in the end this last big factory interview questions is good, you can have a look, I will not move.
conclusion
Most of the time, we should all be like this, this is the most true. However can understand as far as possible to understand it, first try to make their own reach the public level.
There’s still a question: does the ‘resolve’ and ‘reject’ promises cause memory leaks? I see a lot of debate online, but in the end it all comes down to the browser vendors’ implementation, and as long as the engine is implemented properly, it won’t leak. What do you think?