The purpose of writing this article is to analyze the Promise source code, the cause is also the recent autumn recruit was asked to let handwritten Promise, in addition to see the Promise source code on the Internet or more or less small problems, that is, did not fully follow the Promise/A+ specification.
The code will fully use ES6 syntax, mainly divided into the following modules:
- Holistic analysis (paves the way for code writing)
- Implementation of the first version (the constructor is roughly functional)
- Support for asynchronous and chained calls (perfect for then methods)
- Implementing catch methods
- Realize the Promise. The resolve ()
- Realize the Promise. Reject ()
- Realize the Promise. All ()
- Realize the Promise. Race ()
1. Overall analysis
A Promise is a container with three states: PENDING, FULFILLED and REJECTED. It holds the result of a certain event (usually an asynchronous operation) that will end in the future.
- The container status is not affected by external conditions
- Once the state changes it will never change again, and you can get this result at any time
Here’s how Promise is used:
new Promise((resolve, reject) => { // ... // Execute resolve, Reject}). Then (res => {// resolve corresponding to trigger function execution}, Then (// support chain call res => {}).catch(err => console.log(err)) promise.resolve (); Promise.reject(); Promise.all([promise1, promise2, ...] ).then(); Promise.race([promise1, promise2, ...] ).then();Copy the code
It is not difficult to say that:
- The Promise constructor takes a function parameter, exector, which takes resolve and reject and executes it immediately, changing the state through resolve/reject
- When the state changes, trigger on the prototype chain
Then, the catch
methods - The Promise class has static methods
Resolve, Reject, All, and race
You can write roughly structured code:
class Promise {
constructor(exector) {
const resolve = () = >{}const reject = () = > {
}
exector(resolve, reject);
}
then(){}catch() {}static resolve(){}static reject(){}static all(){}static race(){}}Copy the code
You can then add code based on this.
Second, the implementation of the first version
The resolve and reject functions are refined by introducing three states, and exector(resolve, reject) is executed within the constructor:
// Define three states
const PENDING = 'PENDING'; / /
const FULFILLED = 'FULFILLED'; / / has been successful
const REJECTED = 'REJECTED'; / / has failed
class Promise {
constructor(exector) {
// Initialization state
this.status = PENDING;
// Place success and failure results on this for easy access to then and catch
this.value = undefined;
this.reason = undefined;
const resolve = value= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value; }}const reject = reason= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason; }}Execute exector immediately
// Pass internal resolve and reject to the executor. The user can call resolve and rejectexector(resolve, reject); }}Copy the code
Exector (resolve, reject); Execution may fail, so use try, reject, and reject.
constructor(exector) {
// Initialization state
this.status = PENDING;
// Place success and failure results on this for easy access to then and catch
this.value = undefined;
this.reason = undefined;
const resolve = value= > {
if (this.status === PENDING) {
// You can change a state only if it is in progress
this.status = FULFILLED;
this.value = value; }}const reject = reason= > {
if (this.status === PENDING) {
// You can change a state only if it is in progress
this.status = REJECTED;
this.reason = reason; }}// Modify the code
try {
Execute executor immediately
// Pass internal resolve and reject to the executor. The user can call resolve and reject
exector(resolve, reject);
} catch(e) {
// The executor executes an error, throwing out the error content rejectreject(e); }}Copy the code
This will be a pity and REJECTED state. Then receives two functions, which correspond to the FULFILLED and REJECTED states:
new Promise().then(
res => {},
err => {},
)
Copy the code
Note: Then and catch are microtasks, where setTimeout is used to simulate:
then(onFulfilled, onRejected) {
// Then is a microtask, which is simulated by setTimeout
setTimeout(() = > {
if (this.status === FULFILLED) {
// This is a big pity
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// This parameter is executed only in the REJECTED state
onRejected(this.reason); }})}Copy the code
OK, the first version is complete:
// Define three states
const PENDING = 'PENDING'; / /
const FULFILLED = 'FULFILLED'; / / has been successful
const REJECTED = 'REJECTED'; / / has failed
class Promise {
constructor(exector) {
// Initialization state
this.status = PENDING;
// Place success and failure results on this for easy access to then and catch
this.value = undefined;
this.reason = undefined;
const resolve = value= > {
if (this.status === PENDING) {
// You can change a state only if it is in progress
this.status = FULFILLED;
this.value = value; }}const reject = reason= > {
if (this.status === PENDING) {
// You can change a state only if it is in progress
this.status = REJECTED;
this.reason = reason; }}try {
Execute executor immediately
// Pass internal resolve and reject to the executor. The user can call resolve and reject
exector(resolve, reject);
} catch(e) {
// The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
// Then is a microtask, which is simulated by setTimeout
setTimeout(() = > {
if (this.status === FULFILLED) {
// This is a big pity
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// This parameter is executed only in the REJECTED state
onRejected(this.reason); }}}})Copy the code
You can test it with data:
const promise = new Promise((resolve, reject) = > {
Math.random() < 0.5 ? resolve(1) : reject(-1);
}).then(
res= > console.log(res),
err= > console.log(err),
)
Copy the code
Support asynchronous and chain call
At this point, the first version still needs to be improved in three directions:
- Problems with asynchronous code execution within promises.
- The chain call to Promise
- The value passed through
Support for asynchronous code
In development, the interface is often placed inside the promise. When the interface request response is successful, the data is resolved or rejected, and then and catch will be captured.
However, in the current code, resolve will be executed only after asynchronous code is executed in promise, and then will not wait for the completion of asynchronous code execution, so the state is PENDING and the callback function of THEN will not be triggered.
Added onledCallbacks, onRejectedCallbacks Maintenance success state, failure state task queue:
// Define three states
const PENDING = 'PENDING'; / /
const FULFILLED = 'FULFILLED'; / / has been successful
const REJECTED = 'REJECTED'; / / has failed
class Promise {
constructor(exector) {
// Initialization state
this.status = PENDING;
// Place success and failure results on this for easy access to then and catch
this.value = undefined;
this.reason = undefined;
// Add code:
// Success callback function queue
this.onFulfilledCallbacks = [];
// Failed state callback function queue
this.onRejectedCallbacks = [];
const resolve = value= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// Add code:
// The success state functions are executed in sequence
this.onFulfilledCallbacks.forEach(fn= > fn(this.value)); }}const reject = reason= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// Add code:
// The failed state functions are executed in sequence
this.onRejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
try {
Execute executor immediately
// Pass internal resolve and reject to the executor. The user can call resolve and reject
exector(resolve, reject);
} catch(e) {
// The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
// Then is a microtask, which is simulated by setTimeout
setTimeout(() = > {
// Add code:
if (this.status === PENDING) {
// State is PENDING
// Indicate that the promise has asynchronous code execution inside and has not changed its state. Add it to the success/failure callback task queue
this.onFulfilledCallbacks.push(onFulfilled);
this.onRejectedCallbacks.push(onRejected);
}else if (this.status === FULFILLED) {
// This is a big pity
onFulfilled(this.value);
} else if (this.status === REJECTED) {
// This parameter is executed only in the REJECTED state
onRejected(this.reason); }}}})const promise = new Promise((resolve, reject) = > {
setTimeout(() = > resolve(1), 1000);
}).then(
res= > console.log(res)
)
/ / 1
Copy the code
Implement chain calls
A big advantage of promises is that they support chained calls, specifically implementations of then methods that actually return a Promise. A few points to note:
- Save a reference to the previous Promise instance, that is, save
this
- According to the
then
The return value of the callback function execution
- If it is a PROMISE instance, the next promise instance returned will wait for the promise state to change
- If it is not a Promise instance, execute it as is
resolve
orreject
Perfecting the then function:
then(onFulfilled, onRejected) {
/ / save this
const self = this;
return new Promise((resolve, reject) = > {
if (self.status === PENDING) {
self.onFulfilledCallbacks.push(() = > {
// try to catch an error
try {
// Simulate microtasks
setTimeout(() = > {
const result = onFulfilled(self.value);
// There are two kinds of cases:
// 1. The return value of the callback is Promise
// 2. If it is not a Promise, call the new Promise's resolve function
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
// Do the following
try {
setTimeout(() = > {
const result = onRejected(self.reason);
// Reject: reject
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
setTimeout(() = > {
try {
const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
} catch(e) { reject(e); }}); }else if (self.status === REJECT){
setTimeout(() = > {
try {
const result = onRejected(self.error);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
} catch(e) { reject(e); }})}})}Copy the code
The value passed through
Promise support values through:
let promsie = new Promise((resolve,reject) = >{
resolve(1)
})
.then(2)
.then(3)
.then(value= > {
console.log(value)
})
/ / 1
Copy the code
The then argument is expected to be a function, and passing in a non-function will result in value penetration. Value pass-through can be understood as saying that a THEN is invalid if it is passed to something other than a function.
The principle is that if the promise passed in then is not a function, then the promise returns the value of the previous promise. This is how value penetration occurs, so you only need to set the two parameters of then:
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function'? onRejected:
reason= > { throw new Error(reason instanceof Error ? reason.message:reason) }
Copy the code
The complete then function code:
then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function'? onRejected:
reason= > { throw new Error(reason instanceof Error ? reason.message:reason) }
/ / save this
const self = this;
return new Promise((resolve, reject) = > {
if (self.status === PENDING) {
self.onFulfilledCallbacks.push(() = > {
// try to catch an error
try {
// Simulate microtasks
setTimeout(() = > {
const result = onFulfilled(self.value);
// There are two kinds of cases:
// 1. The return value of the callback is Promise
// 2. If it is not a Promise, call the new Promise's resolve function
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
// Do the following
try {
setTimeout(() = > {
const result = onRejected(self.reason);
// Reject: reject
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
try {
setTimeout(() = > {
const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
});
} catch(e) { reject(e); }}else if (self.status === REJECTED){
try {
setTimeout(() = > {
const result = onRejected(self.reason);
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}}); }Copy the code
Implement the catch() method
Promise.prototype. Catch = promise.prototype. Then (null, onRejected)
catch(onRejected) {
return this.then(null, onRejected);
}
Copy the code
Five, the Promise. The resolve ()
Regardless of the thenable object as the parameter, there are two cases for the parameter:
Promise
The instance- not
Promise
The instance
static resolve(value) {
if (value instanceof Promise) {
// If it is a Promise instance, return it directly
return value;
} else {
// If it is not a Promise instance, return a new Promise object, which is FULFILLED
return new Promise((resolve, reject) = >resolve(value)); }}Copy the code
Six, Promise. Reject ()
Promise.reject also returns an instance of Promise with the state REJECTED.
Unlike promise.resolve, the arguments to the promise.reject method are left as reject arguments
static reject(reason) {
return new Promise((resolve, reject) = >{ reject(reason); })}Copy the code
Seven, Promise. All ()
Return a promise object. The returned promise state is successful only if all promises are successful.
- All promise states change to
FULFILLED
, the returned Promise state becomesFULFILLED
. - A promise state changes to
REJECTED
, the returned promise state becomesREJECTED
. - Not all of the array members are promises, you need to use them
Promise.resolve()
To deal with.
static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// Record the number of successfully implemented promises
let count = 0;
return new Promise((resolve, reject) = > {
for (let i = 0; i < len; i++) {
// promise.resolve () to ensure that each is a Promise instance
Promise.resolve(promiseArr[i]).then(
val= > {
values[i] = val;
count++;
// The state of the return promise can be changed if all execution is completed
if (count === len) resolve(values);
},
err= >reject(err), ); }})}Copy the code
Eight, Promise. Race ()
Promise.race() is a simpler implementation:
static race(promiseArr) {
return new Promise((resolve, reject) = > {
promiseArr.forEach(p= > {
Promise.resolve(p).then(
val= > resolve(val),
err= > reject(err),
)
})
})
}
Copy the code
Ix. Complete code
// Simulate the implementation of Promise
// Promise solves callback hell with three tools:
// 1. Callback function delay binding
// 2. Return value through
// 3. Error bubbling
// Define three states
const PENDING = 'PENDING'; / /
const FULFILLED = 'FULFILLED'; / / has been successful
const REJECTED = 'REJECTED'; / / has failed
class Promise {
constructor(exector) {
// Initialization state
this.status = PENDING;
// Place success and failure results on this for easy access to then and catch
this.value = undefined;
this.reason = undefined;
// Success callback function queue
this.onFulfilledCallbacks = [];
// Failed state callback function queue
this.onRejectedCallbacks = [];
const resolve = value= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = FULFILLED;
this.value = value;
// The success state functions are executed in sequence
this.onFulfilledCallbacks.forEach(fn= > fn(this.value)); }}const reject = reason= > {
// You can change a state only if it is in progress
if (this.status === PENDING) {
this.status = REJECTED;
this.reason = reason;
// The failed state functions are executed in sequence
this.onRejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
try {
Execute executor immediately
// Pass internal resolve and reject to the executor. The user can call resolve and reject
exector(resolve, reject);
} catch(e) {
// The executor executes an error, throwing out the error content rejectreject(e); }}then(onFulfilled, onRejected) {
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value;
onRejected = typeof onRejected === 'function'? onRejected :
reason= > { throw new Error(reason instanceof Error ? reason.message : reason) }
/ / save this
const self = this;
return new Promise((resolve, reject) = > {
if (self.status === PENDING) {
self.onFulfilledCallbacks.push(() = > {
// try to catch an error
try {
// Simulate microtasks
setTimeout(() = > {
const result = onFulfilled(self.value);
// There are two kinds of cases:
// 1. The return value of the callback is Promise
// 2. If it is not a Promise, call the new Promise's resolve function
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}); self.onRejectedCallbacks.push(() = > {
// Do the following
try {
setTimeout(() = > {
const result = onRejected(self.reason);
// Reject: reject
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }})}else if (self.status === FULFILLED) {
try {
setTimeout(() = > {
const result = onFulfilled(self.value);
result instanceof Promise ? result.then(resolve, reject) : resolve(result);
});
} catch(e) { reject(e); }}else if (self.status === REJECTED) {
try {
setTimeout(() = > {
const result = onRejected(self.reason);
result instanceof Promise? result.then(resolve, reject) : resolve(result); })}catch(e) { reject(e); }}}); }catch(onRejected) {
return this.then(null, onRejected);
}
static resolve(value) {
if (value instanceof Promise) {
// If it is a Promise instance, return it directly
return value;
} else {
// If it is not a Promise instance, return a new Promise object, which is FULFILLED
return new Promise((resolve, reject) = >resolve(value)); }}static reject(reason) {
return new Promise((resolve, reject) = >{ reject(reason); })}static all(promiseArr) {
const len = promiseArr.length;
const values = new Array(len);
// Record the number of successfully implemented promises
let count = 0;
return new Promise((resolve, reject) = > {
for (let i = 0; i < len; i++) {
// promise.resolve () to ensure that each is a Promise instance
Promise.resolve(promiseArr[i]).then(
val= > {
values[i] = val;
count++;
// The state of the return promise can be changed if all execution is completed
if (count === len) resolve(values);
},
err= >reject(err), ); }})}static race(promiseArr) {
return new Promise((resolve, reject) = > {
promiseArr.forEach(p= > {
Promise.resolve(p).then(
val= > resolve(val),
err= > reject(err),
)
})
})
}
}
Copy the code