Promise, as a new feature in the ES6 series, is undoubtedly a top priority in front-end development. In 2021, Promise is everywhere in both open source projects and business code. This article rewrites A complete version of Promise and its static methods based on the A+ specification to deepen my understanding of asynchronous code.
Write a pre –
It’s all written code, so you must have used it a lot and be familiar with it. Here’s a review of the Promise syntax and static methods.
Grammar knowledge
- Create it directly through new (constructor) or static methods such as promise.resolve ()
- There are only three states (
Pending [initial waiting state]
.Depressing [state of success]
.Rejected [failed state]
) - Once the Promise status is confirmed,
irreversible
. Cannot be modified again - Chain calls through the then/catch/finally methods all return new promises
- The two callback parameters resolve and reject are the success and failure callbacks, respectively
- Perform then (..) Register callback processing arrays (then methods can be called multiple times by the same Promise)
- The essence is to execute in a microtask queue
(Flow chart of Promise execution from MDN)
Methods:
-
Prototype. Then (onFulfilled, onRejected) adds a successful or failed callback to the current Promise and returns a new Promise
-
Promise.prototype. Catch (onRejected) adds the failed callback to the current Promise and returns a new Promise
-
Promise. Prototype. Finally (onFinally) add a callback to the current Promise (regardless of success or failure)
Static methods (all return new Promises) :
-
Promise.resolve(value) : Returns a Promise object with a status of success and passes the given success information to the corresponding handler
-
Promise.reject(reason) : Returns a Promise object in a failed state and passes the given failure information to the corresponding handler
-
Promise.all(iterable) : Success is triggered when all of the Promise objects in the iterable argument succeed, or if one of them fails
-
Promise. AllSettled (Iterable) : Iterable parameter Return after all Promises are complete (including success and failure)
-
Promise.any(iterable) : Receives a collection of Promise objects and returns the value of that successful Promise when one of them succeeds
-
Promise.race(iterable) : Receives a collection of Promise objects and returns the Promise value when one Promise succeeds or fails
Handwritten code
Es6 syntax is used here to build promises
The basic structure
We know that Promise has three states, defined in terms of static constants
const PENDING = "pending";
const RESOLVE = "fulfilled";
const REJECT = "rejected";
Copy the code
Use the class syntax to define a Promise and initialize the state. Since the then method of the same Promise can be called multiple times, the failed and successful callbacks are arrays. When a new Promise is called, a function is passed that has two methods: the successful and failed callback. We execute the incoming callback by defining an Executor. If an error is passed in, the error is caught directly with a try catch and returned with a reject message.
class Promise {
constructor(execurte) {
// The default state is wait
this.status = PENDING;
// Default value for successful callback
this.value = undefined;
// Default value for failed callback
this.resaon = undefined;
// The successful callback queue can be multiple times then, so there are multiple defined as arrays
this.resolveCallBacks = [];
// Failed callback
this.rejecteCallBacks = [];
// Exception handling for actuators
try {
execurte(this.resolve, this.reject);
} catch (error) {
this.reject(error); }}}Copy the code
With the basic structure written, we can see that we are ready for the first step of creation using the following example code.
let promise = new Promise((resolve, reject) = > {
// executor (" Hello Promise")
});
Copy the code
During executor execution. We need to process the contents of resolve, reject, and process the state and data. If the current state is pending (that is, the initialization state), then the corresponding state we modify is success or failure. Save the success or failure values and execute them in the callback queue. With queueMicrotask we perform microtasks
resolve = (value) = > {
queueMicrotask(() = > {
if (this.status === PENDING) {
this.status = FULFILLED; // Change the status
this.value = value;
this.resolveCallBacks.forEach((fn) = > fn(this.value)); // Successful callback}}); };// Callback on failure
reject = (resaon) = > {
queueMicrotask(() = > {
if (this.status === PENDING) {
this.status = REJECTED; // Change the status
this.resaon = resaon;
this.rejecteCallBacks.forEach((fn) = > fn(this.resaon)); // Failed callback}}); };Copy the code
At this point, the basic Promise structure is done, and then the all-important chain call is implemented
Chain calls
We know that promises implement chained calls through then, so to summarize the rules:
- Each then returns a new Promise
- Accept two parameters, both of type Function, to be called when Promise changes state. The first parameter will run in the resolved state and accept the result, and the second parameter will run in the Rejected state and accept the result
- In addition to receiving two function arguments, the then can continue after the then
- Then may return a normal value or a Promise
In the then process, we may directly return a normal value, such as return 100, or we may return a Promise, such as return new Promise(), so we define a utility function _returnValue to handle the returned value. Note that one of the possible scenarios is as follows. Creating the current object and then returning the current object’s condition then creates a circular reference. So you need to add a logical processing of circular references.
// Cyclic reference test
let p1 = new MyPromise((resolve, reject) = > {
resolve("yes");
});
let p2 = p1.then((res) = > {
return p2;
});
// From this then, a circular reference is generated
p2.then(
() = > {},
(reson) = > {
console.log(reson); });Copy the code
You then get a utility function that handles the return value, _returnValue
/ * * * *@param {*} P Currently running Promise *@param {*} CallbackValue Returns the value (then) *@param {*} Resolve Successful callback *@param {*} Reject Failed callback *@returns* /
_returnValue(p, callbackValue, resolve, reject) {
// If p is equal to callbackValue, a circular reference is generated
if (p === callbackValue) {
return reject(new TypeError('Pretty boy, your code is looping'))}// Check whether callbackValue is a Promise type
if (callbackValue instanceof MyPromise) {
callbackValue.then(value= > resolve(value), resaon= > reject(resaon))
} else {
resolve(callbackValue)
}
}
Copy the code
With utility functions, chain calls to implement THEN can directly handle the results. This method takes two of the previously mentioned function arguments, and if no null value is passed to us, returns a function directly, or throws an error for subsequent THEN procedures. There are three states in the processing of THEN
- The successful state
- The failure state
- Wait state
For both success and failure states, we simply execute the corresponding callback function or catch exception handling. Return the value directly using the _returnValue defined earlier.
ResolveCallBacks and rejecteCallBacks store tasks that need to be executed. If the status is pending, the callback is returned asynchronously. Resolve or Reject is not called yet. Join the microtask queue, wait for the result to return and then execute the desired callback function. Without this step, the synchronous code will complete the Promise directly when an asynchronous task is present in the Promise, without changing the state of the Promise after the asynchronous task is complete.
then = (resolveCallBack, rejecteCallBack) = > {
// If null is passed, the default is passed backwards, so add a default case
resolveCallBack = resolveCallBack ? resolveCallBack : (value) = > value;
// Parameters are optional
rejecteCallBack = rejecteCallBack ? rejecteCallBack : (reason) = > { throw reason; };
let p = new MyPromise((resolve, reject) = > {
// Handle different returns, either a normal value directly or a Promise object for further calls
/ / success
if (this.status === FULFILLED) {
// Start a microtask and wait for p result to return. Otherwise the program will return the value of p
// Exception handling for executed functions
queueMicrotask(() = > {
try {
let callbackValue = resolveCallBack(this.value);
this._returnValue(p, callbackValue, resolve, reject);
} catch(error) { reject(error); }});/ / fail
} else if (this.status === REJECTED) {
queueMicrotask(() = > {
try {
let callbackValue = rejecteCallBack(this.resaon);
this._returnValue(p, callbackValue, resolve, reject);
} catch(error) { reject(error); }});// The waiting process
} else {
// The task is stored and then executed
// Store successful tasks
this.resolveCallBacks.push(() = > {
queueMicrotask(() = > {
try {
let callbackValue = resolveCallBack(this.value);
this._returnValue(p, callbackValue, resolve, reject);
} catch(error) { reject(error); }}); });// Storage failure
this.rejecteCallBacks.push(() = > {
queueMicrotask(() = > {
try {
let callbackValue = rejecteCallBack(this.resaon);
this._returnValue(p, callbackValue, resolve, reject);
} catch(error) { reject(error); }}); }); }});return p;
};
Copy the code
Catch is easy to implement once you’ve done then, calling the THEN method directly and passing in a failed function.
// Register a non-static method to catch error information
catch(rejecteCallBack) {
return this.then(undefined, rejecteCallBack)
}
Copy the code
A finally method is a method that executes whether it succeeds or fails, returning a method directly through THEN
// Register a non-static method, finally executed whether it succeeds or fails
finally(callback) {
return this.then((value) = > {
return MyPromise.resolve(callback()).then(() = > value)
}, (resaon) = > {
return MyPromise.resolve(callback()).then(() = > { throw resaon })
})
}
Copy the code
A static method
resolve
This just returns a Promise of success. Nothing to say
static resolve(value) {
// If it is a Promise object, return it directly
if (value instanceof MyPromise) {
return value
} else {
// If it is not a Promise object, create another one
return new MyPromise((resolve) = > {
resolve(value)
})
}
}
Copy the code
reject
Return a failed Promise
// static method, return false Promise
static reject(resaon) {
if (resaon instanceof MyPromise) {
return resaon
} else {
// If it is not a Promise object, create another one
return new MyPromise((resolve, reject) = > {
reject(resaon)
})
}
}
Copy the code
all
All mainly keeps the results of each one and returns when all the results are complete. Then is used to obtain the Promise of success
static all(promises) {
// Saves an array of callback results
let result = [];
// an accumulator is used to determine whether the execution of the method queue is complete
let count = 0;
// The all method also returns a Promise object
return new MyPromise((resolve, reject) = > {
function pushResult(key, value) {
result[key] = value
count++
// If the accumulator and the list of tasks executed are equal in length, the entire task has been completed
if (count === promises.length) {
resolve(result)
}
}
// Loop through the tasks to be executed
promises.forEach((task, index) = > {
if (task instanceof MyPromise) {
task.then((v) = > pushResult(index, v), (resaon) = > reject(resaon))
} else {
pushResult(index, promises[index])
}
})
})
}
Copy the code
allSettled
AllSettled will return to you regardless of success or failure, so use the finally keyword directly when processing. If it is a common type, save value
static allSettled(promises) {
return new MyPromise((resolve) = > {
let results = []
let count = 0
promises.forEach((task, index) = > {
if (task instanceof MyPromise) {
task.finally(_= > {
count++
results[index] = {
status: task.status,
value: task.value || task.resaon
}
if (count === promises.length) {
resolve(results)
}
})
} else {
count++
results[index] = {
status: 'fulfilled'.value: task
}
if (count === promises.length) {
resolve(results)
}
}
})
})
}
Copy the code
any
Return as long as there is a success, again using then
static any(promises) {
return new MyPromise((resolve) = > {
promises.forEach((task) = > {
if (task instanceof MyPromise) {
task.then(_= > {
resolve(task.value)
})
} else {
resolve(task)
}
})
})
}
Copy the code
race
As long as there is a success or return, use finally to get
static race(promises) {
return new MyPromise((resolve) = > {
promises.forEach((task) = > {
if (task instanceof MyPromise) {
task.finally(_= > {
resolve(task.value || task.resaon)
})
} else {
resolve(task)
}
})
})
}
Copy the code
Adding a Test Configuration
Then add the defer use case and execute the test case
promises-aplus-tests Promise.js
Copy the code
MyPromise.defer = MyPromise.deferred = function () {
let testObj = {}
testObj.promise = new Promise((resolve, reject) = > {
testObj.resolve = resolve
testObj.reject = reject
})
return testObj
}
module.exports = MyPromise
Copy the code
The complete code
Complete code GitHub address