Promise is a commonly used syntax in development. Basically, asynchronous processing is mostly completed through Promise. There are many Promise specifications, and ES6 ultimately uses the Promise/A+ specification, so the following code is written largely based on that specification.
Let’s start by enumerating all of Promise’s instance methods and static methods
Instance methods
- then:
new Promise((resolve, reject) => {... }). Then (() = > {the console. The log (' rsolve success callback ')}, () = > {the console. The log (' failed to reject the callback ')})
- catch:
new Promise((resolve, reject) => {... }).catch(() => {console.log('reject ')})
- finally:
new Promise((resolve, reject) => {... }).finally(() => {console.log(' enter both success and failure ')})
- All of the above method calls will return new
Promise
A static method
- resolve:
Promise.resolve(value)
returnPromise
The instance - reject:
Promise.reject(value)
returnPromise
The instance - all:
Promise.all(promises)
: passed in array formatPromise
And return a newPromise
For instance, success returns the values in order, and one failure becomes a failure - race:
Promise.race(promises)
: passed in array formatPromise
And return a newPromise
For example, success or failure depends on the way the first one is done
Once the Promise state is confirmed to be changed, it cannot be changed again. It has the following three states: Pending, depressing and Rejected Promise are implemented in the micro-task queue in the browser and need to process the micro-task (Event Loop mechanism in JavaScript).
1. Declare the instance method of the Promise
class Promise {
_value
_state = 'pending'
_queue = []
constructor(fn) {
if (typeoffn ! = ='function') {
throw 'Promise resolver undefined is not a function'
}
/* new Promise((resolve, reject) => {resolve: reject: fail}) */
fn(this._resolve.bind(this), this._reject.bind(this))}// Take 1-2 arguments, the first for a successful callback and the second for a failed callback
then(onFulfilled, onRejected) {
// It might already be resolved, because promises can be resolved early and then registered after the then method
if (this._state === 'fulfilled') { onFulfilled? . (this._value)
return
}
/ / reject again
if (this._state === 'rejected') { onRejected? . (this._value)
return
}
// The Promise isn't finished yet, so push it to a queue. When it's finished, execute the corresponding function in the queue
this._queue.push({
onFulfilled,
onRejected,
})
}
// Failed to receive callback
catch(onRejected) {
// This is equivalent to calling then directly and passing in a failed callback
this.then(null, onRejected)
}
// Callback executed on both success and failure
finally(onDone) {
const fn = () = > onDone()
this.then(fn, fn)
}
/ / the resolve to success
_resolve(value) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
this._state = 'fulfilled'
// Just store the value and fetch it when called again, because once a Promise is made, it won't change
this._value = value
// Execute the push function argument in the previous. Then method, so that the corresponding method is executed.
this._queue.forEach((callback) = >{ callback.onFulfilled? . (this._value)
})
}
/ / reject failure
_reject(error) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
this._state = 'rejected'
this._value = error
this._queue.forEach((callback) = >{ callback.onRejected? . (this._value)
})
}
}
Copy the code
Call logic:
-
OnFulfilled => then((onFulfilled, onFulfilled) => {… })
-
Place the ondepressing function in the _queue collection in the then method. => this._queue.push({ onFulfilled, onRejected })
-
Once the async callback is complete, resolve is executed, at which point the functions collected by _queue registered with the then method are called. Execute these functions in unison so that the asynchronous callback completes, executing the corresponding functions in the THEN method
// Print the result
const p = new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve('success')},1000)
})
p.then((res) = > {
console.log(res) // => success
})
// reject
const p1 = new Promise((resolve, reject) = > {
setTimeout(() = > {
reject('fail')},1000)
})
p1.catch((res) = > {
console.log(res) // => fail
})
// finally
const p2 = new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve()
}, 1000)
})
p2.finally(() = > {
console.log('done') // => done
})
Copy the code
Online code demo
2. Microtask and return promises
A. Perform microtasks
In the browser, promises are pushed to the microtask after completion, so we need to do that as well. Using MutationObserver in the browser, Node can use process.nexttick
class Promise {...// Push into microtasks
_nextTick(fn) {
if (typeofMutationObserver ! = ='undefined') { // The browser implements microtasks through MutationObserver
// This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
const observer = new MutationObserver(fn)
let count = 1
const textNode = document.createTextNode(String(count))
observer.observe(textNode, {
characterData: true
})
textNode.data = String(++count)
} else if (typeofprocess.nextTick ! = ='undefined') { // The node side uses process.nextTick
process.nextTick(fn)
} else {
setTimeout(fn, 0)}}/ / the resolve to success
_resolve(value) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
// Push into microtasks
this._nextTick(() = > {
this._state = 'fulfilled'
this._value = value
this._queue.forEach((callback) = >{ callback.onFulfilled? . (this._value)
})
})
}
/ / reject failure
_reject(error) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
// Push into microtasks
this._nextTick(() = > {
this._state = 'rejected'
this._value = error
this._queue.forEach((callback) = >{ callback.onRejected? . (this._value)
})
})
}
...
}
Copy the code
Results demonstrate
B. Return the Promise to make the chain call
Typically promises handle multiple asynchronous requests, sometimes with interdependencies.
Such as:
const getUser = () = > {
return new Promise((resolve, reject) = > {
setTimeout(() = > {
resolve({
userId: '123'})},500)})}const getDataByUser = (userId) = > {
return new Promise((resolve, reject) = > {
setTimeout(() = > {
/ /...
resolve({a: 1})},500)})}/ / use
getUser().then((user) = > {
return getDataByUser(user.userId)
}).then((res) = > {
console.log(res)// {a: 1}
})
Copy the code
GetDataByUser relies on getUser to request the user information back. Here we need to use the Promise chain call. Let’s change our code
class Promise {
constructor(fn) {
fn(this._resolve.bind(this), this._reject.bind(this))}...// 1. The then method needs to return a new Promise, because the chained call is required and the next THEN method accepts the value of the previous THEN method
// 2. The returned Promise must be a new Promise, otherwise the state and result will be shared.
// 3. Treat the return value from the previous THEN method as the value of the next Promise resolve
then(onFulfilled, onRejected) {
// Return a new Promise
return new Promise((resolve, reject) = > {
// It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
if (this._state === 'fulfilled' && onFulfilled) {
this._nextTick(onFulfilled.bind(this.this._value))
return
}
if (this._state === 'rejected' && onRejected) {
this._nextTick(onRejected.bind(this.this._value))
return
}
/* Associate the parameters of the current Promise's then method with the new Promise's resolve, reject. This is a big pity. This will be a big pity. This is a big pity that can associate the onFulfilled Promise with the resolve in the new Promise. Reject the same * /
this._queue.push({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
/ / reject again
_resolve(value) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
// This example returns a Promise, not a Promise, so we need to do something special here.
If it is a Promise object, we need to parse the Promise result and then pass the value to resolve
if (typeof value === 'object' && typeof value.then === 'function') {
// We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
// Get the Promise value
// this._resove => obj.onFulfilled? .(this._value)
// this._reject => obj.onRejected? .(this._value)
value.then(this._resolve.bind(this), this._reject.bind(this))
return
}
// Push into microtasks
this._nextTick(() = > {
this._state = 'fulfilled'
this._value = value
this._queue.forEach((obj) = > {
// Accept ondepressing return value
constval = obj.onFulfilled? . (this._value)
Then ((res) => {consolle.log(res)})
Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
obj.resolve(val)
})
})
}
...
}
Copy the code
Results demonstrate
Call logic:
-
Microtasks are implemented using MutationObserver and process.nextTick
-
The Promise is called chained, by associating the (onFulfilled, onRejected) parameter in the THEN method with the (resolve, reject) parameter in the newly returned Promise.
-
Once the previous Promise is fulfilled, the ondepressing function is called, and the value returned in the ondepressing function can be put into the new Promise’s resolve.
-
If you encounter a resolve value that is a Promise object, parse recursively and return the value
The complete code
class Promise {
_value
_state = 'pending'
_queue = []
constructor(fn) {
if (typeoffn ! = ='function') {
throw new Error('Promise resolver undefined is not a function')}/* new Promise((resolve, reject) => {resolve: reject: fail}) */
fn(this._resolve.bind(this), this._reject.bind(this))}// Take 1-2 arguments, the first for a successful callback and the second for a failed callback
then(onFulfilled, onRejected) {
// Return a new Promise
return new Promise((resolve, reject) = > {
// It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
if (this._state === 'fulfilled' && onFulfilled) {
this._nextTick(onFulfilled.bind(this.this._value))
return
}
if (this._state === 'rejected' && onRejected) {
this._nextTick(onRejected.bind(this.this._value))
return
}
// Associate the parameters of the current Promise's then method with the new Promise's resolve, reject
this._queue.push({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
// Failed to receive callback
catch(onRejected) {
return this.then(null, onRejected)
}
// Callback executed on both success and failure
finally(onDone) {
return this.then((value) = > {
onDone()
return value
}, (value) = > {
// console.log(value)
onDone()
throw value
})
}
// Push into microtasks
_nextTick(fn) {
if (typeofMutationObserver ! = ='undefined') { / / the browser
// This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
const observer = new MutationObserver(fn)
let count = 1
const textNode = document.createTextNode(String(count))
observer.observe(textNode, {
characterData: true
})
textNode.data = String(++count)
} else if (typeofprocess.nextTick ! = ='undefined') { // node
process.nextTick(fn)
} else {
setTimeout(fn, 0)}}/ / the resolve to success
_resolve(value) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
// This example returns a Promise, not a Promise, so we need to do something special here.
If resolve() is a Promise object, we need to resolve the Promise result and pass it to resolve
if (typeof value === 'object' && typeof value.then === 'function') {
// We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
// Get the Promise value
// this._resove => obj.onFulfilled? .(this._value)
// this._reject => obj.onRejected? .(this._value)
value.then(this._resolve.bind(this), this._reject.bind(this))
return
}
// Push into microtasks
this._nextTick(() = > {
this._state = 'fulfilled'
this._value = value
this._queue.forEach((obj) = > {
// Use try catch to catch onFulfilled internal errors
try {
// Accept onFulfilled return value. If there is none, pass this._value downward
const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
Then ((res) => {consolle.log(res)})
Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
obj.resolve(val)
} catch (e) {
obj.reject(e)
}
})
})
}
/ / reject failure
_reject(error) {
if (this._state ! = ='pending') return
this._nextTick(() = > {
this._state = 'rejected'
this._value = error
this._queue.forEach((obj) = > {
try {
const val = obj.onRejected ? obj.onRejected(this._value) : this._value
// Reject returns a new Promise after the current reject executes, which should be resolved properly, so resolve should be used instead of continuing with reject to make the next Promise fail
obj.resolve(val)
} catch (e) {
obj.reject(e)
}
})
})
}
}
Copy the code
Static methods that declare promises
Resolve, promise. reject, promise. all, and promise. race are all static methods that return a new Promise.
class Promise {.../** * resolve */
static resolve(value) {
// The Promise returns directly
if (value instanceof Promise) {
return value
} else if (typeof value === 'object' && typeof value.then === 'function') {
// The object passed in contains the then method
const then = value.then
return new Promise((resolve) = > {
then.call(value, resolve)
})
} else {
// Return the new Promise in resolve
return new Promise((resolve) = > resolve(value))
}
}
/**
* 直接reject, 测试下Promise.reject并没做特殊处理,所以直接返回即可。
*/
static reject(value) {
return new Promise((resolve, reject) = > reject(value))
}
/** * passes in an array of 'Promise' and returns a new 'Promise' instance. Success returns the values in order, and one failure becomes a failure */
static all(promises) {
return new Promise((resolve, reject) = > {
let count = 0
let arr = []
// Push into the array with the corresponding subscript
promises.forEach((promise, index) = > {
// Convert to a Promise object
Promise.resolve(promise).then((res) = > {
count++
arr[index] = res
if (count === promises.length) {
resolve(arr)
}
}, err= > reject(err))
})
})
}
/** * passes in an array of 'Promise' and returns a new instance of 'Promise', depending on how the first completes */
static race(promises) {
return new Promise((resolve, reject) = > {
promises.forEach((promise, index) = > {
// Convert to a Promise object
Promise.resolve(promise).then((res) = > {
// Who executes direct resolve, or reject first
resolve(res)
}, err= > reject(err))
})
})
}
...
}
Copy the code
Promise implements the complete code
class Promise {
_value
_state = 'pending'
_queue = []
constructor(fn) {
if (typeoffn ! = ='function') {
throw new Error('Promise resolver undefined is not a function')}/* new Promise((resolve, reject) => {resolve: reject: fail}) */
fn(this._resolve.bind(this), this._reject.bind(this))}/** * takes 1-2 arguments, the first for a successful callback and the second for a failed callback@param {*} onFulfilled
* @param {*} onRejected
* @return {*}
* @memberof Promise* /
then(onFulfilled, onRejected) {
// Return a new Promise
return new Promise((resolve, reject) = > {
// It is possible that the Promise will already resolve, because it can be pre-resolved and registered after the then method, in which case the value can be returned directly to the function
if (this._state === 'fulfilled' && onFulfilled) {
this._nextTick(onFulfilled.bind(this.this._value))
return
}
if (this._state === 'rejected' && onRejected) {
this._nextTick(onRejected.bind(this.this._value))
return
}
// Associate the parameters of the current Promise's then method with the new Promise's resolve, reject
this._queue.push({
onFulfilled,
onRejected,
resolve,
reject
})
})
}
/** * Failed to receive callback **@param {*} onRejected
* @return {*}
* @memberof Promise* /
catch(onRejected) {
return this.then(null, onRejected)
}
/** * Callback executed on both success and failure@param {*} onDone
* @return {*}
* @memberof Promise* /
finally(onDone) {
return this.then((value) = > {
onDone()
return value
}, (value) = > {
onDone()
// The error can be caught in a try catch
throw value
})
}
/** * resolve **@static
* @param {*} value
* @return {*}
* @memberof Promise* /
static resolve(value) {
if (value instanceof Promise) {
return value
} else if (typeof value === 'object' && typeof value.then === 'function') {
// The object passed in contains the then method
const then = value.then
return new Promise((resolve) = > {
then.call(value, resolve)
})
} else {
return new Promise((resolve) = > resolve(value))
}
}
/** * Reject, reject, reject, Promise. Reject@static
* @param {*} value
* @return {*}
* @memberof Promise* /
static reject(value) {
return new Promise((resolve, reject) = > reject(value))
}
/** * passes in an array of 'Promise' and returns a new 'Promise' instance. Success returns the values in order, and one failure becomes a failure@static
* @param {*} promises
* @memberof Promise* /
static all(promises) {
return new Promise((resolve, reject) = > {
let count = 0
let arr = []
if (Array.isArray(promises)) {
if (promises.length === 0) {
return resolve(promises)
}
promises.forEach((promise, index) = > {
// Convert to a Promise object
Promise.resolve(promise).then((res) = > {
count++
arr[index] = res
if (count === promises.length) {
resolve(arr)
}
}, err= > reject(err))
})
return
} else {
reject(`${promises} is not Array`)}}}/** * Passes an array of 'Promise' and returns a new instance of 'Promise', which succeeds or fails depending on how the first completes@static
* @param {*} promises
* @return {*}
* @memberof Promise* /
static race(promises) {
return new Promise((resolve, reject) = > {
if (Array.isArray(promises)) {
promises.forEach((promise, index) = > {
// Convert to a Promise object
Promise.resolve(promise).then((res) = > {
resolve(res)
}, err= > reject(err))
})
} else {
reject(`${promises} is not Array`)}}}// Push into microtasks
_nextTick(fn) {
if (typeofMutationObserver ! = ='undefined') { / / the browser
// This block can be used separately to avoid unnecessary overhead, otherwise nodes need to be generated every time.
const observer = new MutationObserver(fn)
let count = 1
const textNode = document.createTextNode(String(count))
observer.observe(textNode, {
characterData: true
})
textNode.data = String(++count)
} else if (typeofprocess.nextTick ! = ='undefined') { // node
process.nextTick(fn)
} else {
setTimeout(fn, 0)}}/ / the resolve to success
_resolve(value) {
// When the state is fixed, it does not change anymore
if (this._state ! = ='pending') return
// This example returns a Promise, not a Promise, so we need to do something special here.
If resolve() is a Promise object, we need to resolve the Promise result and pass it to resolve
if (typeof value === 'object' && typeof value.then === 'function') {
// We can pass the current _resolve method, because as soon as the next Promise resolve is passed, the then parameter is executed and the value is passed.
// Get the Promise value
// this._resove => obj.onFulfilled? .(this._value)
// this._reject => obj.onRejected? .(this._value)
value.then(this._resolve.bind(this), this._reject.bind(this))
return
}
// In the print test, if resolve is executed directly in the thread, the state and value of resolve appear to be changed directly, without completing the main process.
// So state changes and value changes are moved out of the microtask and are only handled through the microtask when a callback is made
this._state = 'fulfilled'
this._value = value
// Push into microtasks
this._nextTick(() = > {
this._queue.forEach((obj) = > {
// Use try catch to catch onFulfilled internal errors
try {
// Accept onFulfilled return value. If there is none, pass this._value downward
const val = obj.onFulfilled ? obj.onFulfilled(this._value) : this._value
Then ((res) => {consolle.log(res)})
Resolve is the resolve function of the new Promise, which passes the return value from the then method to the next Promise
obj.resolve(val)
} catch (e) {
obj.reject(e)
}
})
})
}
/ / reject failure
_reject(error) {
if (this._state ! = ='pending') return
this._state = 'rejected'
this._value = error
this._nextTick(() = > {
this._queue.forEach((obj) = > {
try {
// An internal error was caught in the function passed by the user
if (obj.onRejected) {
const val = obj.onRejected(this._value)
// Reject returns a new Promise after the current reject executes, which should be resolved properly, so resolve should be used instead of continuing with reject to make the next Promise fail
obj.resolve(val)
} else {
// Pass recursive reject error
obj.reject(this._value)
}
} catch (e) {
obj.reject(e)
}
})
})
}
}
Copy the code
Full demo effect
Blog Post
The complete code of this project: GitHub
This is how Promises work. There are certainly differences between Promises/A+ and Promises. This is just for learning purposes.
QQ group: front end dozen miscellaneous group
Public account: Dongmelon Bookstore