preface
Handwritten Promise, platitude topic, but the actual use, will inevitably step on the pit, today to realize it step by step, feel where it is sacred. If you haven’t written promises yet, or if you’d like to brush up on their implementation, you can check out this article.
The next step is to implement a Promise that complies with the PromiseA+ specification.
Here, PromiseA+
V0: basic implementation
This is a big pity, which must be fulfilled before I forget my Promise. This is a pity, which must be fulfilled before I forget my Promise.
The prototype of the Promise
class MyPromise {
constructor(executor) {
this.status = 'pending' / / state
this.value = undefined // Successful results
this.reason = undefined // Cause of failure
let resolve = () = > {}
let reject = () = > {}
executor(resolve, reject)
}
then(onFulfilled, onRejected){}}Copy the code
Executor fault tolerance
// The incoming execution function may throw an error
try {
// Assign resolve and reject to the user
executor(resolve, reject)
} catch (e) {
// error injection reject
reject(e)
}
Copy the code
Resolve and Reject methods
/ / define the resolve
let resolve = data= > {
// Can only change from Pending to depressing or Rejected
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = data
}
}
/ / define reject
let reject = data= > {
// Can only change from Pending to depressing or Rejected
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = data
}
}
Copy the code
Then method
// Define the then method, passing the result of the fulfilled or rejected into onFulfilled or rejected
then(onFulfilled, onRejected) {
if (this.status === 'fulfilled') {
onFulfilled(this.value)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
}
Copy the code
So, a simple implementation is complete.
Test:
let p = new MyPromise((resolve, reject) = > resolve(1))
p.then(res= > console.log(res)) / / 1
Copy the code
The above example works fine, but what if promises are asynchronous internally?
let q = new MyPromise((resolve, reject) = > {
setTimeout(() = > {
resolve(2)},0)
})
q.then(res= > console.log(res)) // There is no output
Copy the code
Due to asynchronous delay, when calling the THEN method, the state is still pending, and ondepressing or onRejected cannot be called. Therefore, we need to deal with the asynchronous case accordingly.
V1: Add asynchronous processing
How do I handle asynchrony?
This is a big pity/Pity. The callback function should be saved until the state changes (fulfilled/Rejected). This is a big pity/Pity. Given the possibility of multiple callback functions, we use arrays to store callback functions, forming callback queues.
1. Define two arrays as callback queues
this.onResolvedCallbacks = [] // Store the successful callback
this.onRejectedCallbacks = [] // Store the failed callback
Copy the code
2. The then method handles callback functions in pending state
then(onFulfilled, onRejected) {
if (this.status === 'fulfilled') {
onFulfilled(this.value)
}
if (this.status === 'rejected') {
onRejected(this.reason)
}
// New: Store callback functions when Promise is still in wait state
if (this.status === 'pending') {
this.onResolvedCallbacks.push(onFulfilled)
this.onRejectedCallbacks.push(onRejected)
}
}
Copy the code
3. Invoke the callback queue function
When is the callback queue invoked?
Since resolve or Reject is in an asynchronous queue, we already store the corresponding callback function in THEN, so when the state changes, that is, when resolve or Reject occurs, the callback function can be taken out and called in sequence.
Modify the constructor method:
let resolve = data= > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.value = data
// When the state changes, the functions that fetch the callback queue are called in sequence
this.onResolvedCallbacks.forEach(cb= > cb(this.value))
}
}
let reject = data= > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = data
// When the state changes, the functions that fetch the callback queue are called in sequence
this.onRejectedCallbacks.forEach(cb= > cb(this.reason))
}
}
Copy the code
When we store the callback function, value or Reason does not have a value until the state changes, so we pass in the corresponding value or Reason when we call the callback queue function in turn.
Test to see if adding asynchrony works
let q = new MyPromise((resolve, reject) = > {
setTimeout(() = > {
resolve(2)},0)
})
q.then(res= > console.log(res)) / / 2
Copy the code
Asynchronous processing, complete.
Is this it? But don’t forget the chain call of Promise!
V2: Implement chain call
There is no way to implement chain calls based on the previous implementation.
q.then(res= > {
console.log(res)
return 3
}).then(res= > console.log(res)) // Uncaught TypeError: Cannot read property 'then' of undefined
Copy the code
Don’t forget Promises/A+ specification: The then method returns A new Promise.
Next, refine the then method
Then parameters: onFulfilled and onRejected
// Onpity and onRejected are functions
// Ondepressing is not a function that wraps as a function and returns the passed value
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : value= > value
// If onRejected is not a function, an error must be thrown; otherwise, resolve will be caught in the chain call
onRejected =
typeof onRejected === 'function' ? onRejected : error= > {throw error}
Copy the code
The then method returns a new Promise and adds a try… catch
then(onFulfilled, onRejected) {
// Onpity and onRejected are functions
// Ondepressing is not a function that wraps as a function and returns the passed value
onFulfilled =
typeof onFulfilled === 'function' ? onFulfilled : value= > value
// If onRejected is not a function, you need to throw an error, otherwise resolve will be caught in the chain call!!
onRejected =
typeof onRejected === 'function'
? onRejected
: error= > {
throw error
}
const promise = new MyPromise((resolve, reject) = > {
if (this.status === 'fulfilled') {
try {
let x = onFulfilled(this.value)
resolve(x)
} catch (e) {
reject(e)
}
}
if (this.status === 'rejected') {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
}
// When a Promise is still in the wait state, store the callback function
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() = > {
try {
let x = onFulfilled(this.value)
resolve(x)
} catch (e) {
reject(e)
}
})
this.onRejectedCallbacks.push(() = > {
try {
let x = onRejected(this.reason)
resolve(x)
} catch (e) {
reject(e)
}
})
}
})
return promise
}
Copy the code
This is not an error when you do the chain call
q.then(res= > {
console.log(res)
return 3
}).then(res= > console.log(res)) // Output 2 3 at a time
q.then()then().then(res= > console.log(res)) // 2, passed in sequence, so output 2
Copy the code
At this point, you’ve basically implemented the Promise that supports chain calls.
叒 but in onFulfilled and onRejected we can return any value, including the original data type, reference type and even Promise! Based on the above implementation, it is not enough to process all the returned values. Do not believe you:
q.then(res= > {
console.log(res)
return new MyPromise((resolve) = > resolve(3))
}).then(res= > console.log(res)) // Output 2 MyPromise
Copy the code
This is a big pity, but in fact, the Promise will go step by step until it gets a value in the Promise which is fulfilled and onRejected. In other words, he’s actually going to print 2, 3.
This brings us to the next version.
We extracted the logic of resolve from then and replaced it with a complete version of resolvePromise.
V3: Introduce the resolvePromise method
What do we want this function to do?
- Process the return value X of onFulfilled and onRejected. Resolve is required when onFulfilled and reject is required when onFulfilled
- Circular reference: Raises TypeError if the return value promise for THEN is the same reference as x (2.3.1)
The parameters required by resolvePromise
Based on the above analysis, the function looks like this
/** * Process the return value of onFulfilled or onRejected * from then@param {Object} The Promise object * returned by the Promise then method@param {*} X onFulfilled or onRejected returns *@param {Function} The resolve Promise constructor's resolve method *@param {Function} Reject The reject method */ of the reject Promise constructor
function resolvePromise(promise, x, resolve, reject) {
//
}
Copy the code
A TypeError error is raised during a circular reference
if (promise === x) {
return reject(new TypeError('Promise loop referenced '))}Copy the code
Processing return value
Something like this:
// return x as Promise (2.3.2)
// This section can be omitted because it is already covered in the treatment of THEN below
// if (x instanceof Promise) {}
// Return x as an object or function (2.3.3) including x as a Promise (2.3.2)
if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
// try... Catch prevents an exception from then
try {
let then = x.then / / (2.3.3.1)/}catch (e) {
reject(e) / / (2.3.3.2) (2.3.3.3.4.2)}}else {
// The return value x is just a normal value (2.3.4)
resolve(x)
}
Copy the code
Next, deal with the complex scenario of the return value
// return x as Promise (2.3.2)
// This section can be omitted because it is already covered in the treatment of THEN below
// if (x instanceof Promise) {}
let called = false // resolve or reject
// Return x as an object or function (2.3.3) including x as a Promise (2.3.2)
if(x ! = =null && (typeof x === 'object' || typeof x === 'function')) {
// try... Catch prevents an exception from then
try {
let then = x.then / / (2.3.3.1)
if (typeof then === 'function') {
// Return value x has then and then is a function, call it and point this to x (2.3.3.3)
then.call(
x,
y= > {
if (called) return Resolve or reject (2.3.3.3.3)
called = true
// y may still be a Promise, recursive
resolvePromise(promise, y, resolve, reject)
},
r= > {
if (called) return Resolve or reject (2.3.3.3.3)
called = true
reject(r)
}
)
} else {
// Resolve is a normal object or function
resolve(x)
}
} catch (e) {
if (called) return Resolve or reject (2.3.3.3.4.1)
called = true
reject(e) / / (2.3.3.2) (2.3.3.3.4.2)}}else {
// The return value x is just a normal value (2.3.4)
resolve(x)
}
Copy the code
V4: Add asynchronous delay to all returns in THEN (standard version)
SetTimeout is used here to simulate the delay
then(onFulfilled, onRejected) {
// ...
let promise
promise = new MyPromise((resolve, reject) = > {
if (this.status === 'fulfilled') {
setTimeout(() = > {
try {
let x = onFulfilled(this.value)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)}if (this.status === 'rejected') {
setTimeout(() = > {
try {
let x = onRejected(this.reason)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)}// When a Promise is still in the wait state, store the callback function
if (this.status === 'pending') {
this.onResolvedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onFulfilled(this.value)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)})this.onRejectedCallbacks.push(() = > {
setTimeout(() = > {
try {
let x = onRejected(this.reason)
resolvePromise(promise, x, resolve, reject)
} catch (e) {
reject(e)
}
}, 0)})}})return promise
}
Copy the code
Standard version implementation: MyPromise
Tests compliance with the PromiseA+ specification
Use promises- aplus-Tests to test whether promises comply with PromiseA+
yarn add promises-aplus-tests
Copy the code
Before testing, end your Promise with:
MyPromise.defer = MyPromise.deferred = function () {
let defer = {}
defer.promise = new MyPromise((resolve, reject) = > {
defer.resolve = resolve
defer.reject = reject
})
return defer
}
try {
module.exports = MyPromise
} catch (e) {}
Copy the code
Now you can test it
npx promises-aplus-tests Promise.js
Copy the code
Based on the above implementation, you see a series of green checks, culminating in 872 passing (17s), which passes the test.
V5: Plus peripheral methods (modern full version)
In fact, based on V4 is enough, and passed the test. However, when we use Promise, we can directly use Promise. Resolve, Promise, reject and other methods. V5 is based on V4, Added implementations of resolve, Reject, Catch, finally, All, race, allSettled, and so on. This is called the “modern complete Edition”.
Modern full version implementation: Promise_pro
Q & A
1. Why are we talking about this old topic again?
Constant review and review is the last stubbornness of the ungifted. I’d be honored if I could do the same for you.
2. Why use setTimeout to delay all returns in THEN?
First, the Promise itself is synchronous, and its then and catch methods are asynchronous. The use of setTimeout to simulate asynchracy is consistent with the Promise A+ specification and has been tested, but you can also use other methods, such as MutationObserver.
Although setTimeout is a macro task in Eventloop, in reality the then and catch of promises are microtask queues, this implementation is slightly different from reality. But that doesn’t prevent us from understanding how it works by writing A Promise that conforms to the Promise A+ specification.
reference
Thank you for paving the way and helping me move forward
- PromiseA+
- The most readable Promise/A+ ever fully implemented
- Promise is a Promise you can understand