Promise is a new asynchronous solution in ES6, and it is often seen in daily development, as the native FETCH API is implemented based on Promise. So what are the features of promises, and how do you implement A promise with A promise/A+ specification?
Promise features
Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises: Promises
- The state machine
- There are three states: Pending, depressing and Rejected
- This state can only be changed by pending -> depressing and pending -> Rejected. After the state is changed, it cannot be changed again
- There must be an immutable value for success and an immutable reason for failure
- The constructor
- Promise accepts a function as an argument that takes two parameters fulfill and reject
- Fulfill sets the promise state from pending to fulfilled and returns the result of the operation
- Reject sets the promise state from Pending to Rejected, and returns an error
- Then method
- OnFulfilled and onRejected, which indicates the promise success and failure, respectively
- The return value is passed as an argument to the next then method’s argument
- Asynchronous processing
- Chain calls
- Other apis
- The catch, finally
- Resolve, Reject, Race, all, etc
implementation
Next we implement A promise step by step with the PROMISE /A+ specification
Basic implementation
Start by defining a constant that represents the three states of a promise
const STATE = {
PENDING: 'pending'.FULFILLED: 'fulfilled'.REJECTED: 'rejected'
}
Copy the code
Then, two parameters value and Reason are initialized in promise, which respectively represent the value when the state is FULFILL and reject. Then, two functions are defined, which update the state and corresponding field value internally, and execute on success and failure respectively. We then pass these two functions to the constructor’s function arguments as follows:
class MyPromise {
constructor(fn) {
/ / initialization
this.state = STATE.PENDING
this.value = null
this.reason = null
/ / success
const fulfill = (value) = > {
// State can be changed only if state is pending
if (this.state === STATE.PENDING) {
this.state = STATE.FULFILLED
this.value = value
}
}
/ / fail
const reject = (reason) = > {
if (this.state === STATE.PENDING) {
this.state = STATE.REJECTED
this.reason = reason
}
}
Reject is called when there is an error executing the function
try {
fn(fulfill, reject)
} catch (e) {
reject(e)
}
}
}
Copy the code
If the current state is FulfulLED, the callback succeeds; if the current state is Rejected, the callback fails.
class MyPromise {
constructor(fn) {
/ /...
}
then(onFulfilled, onRejected) {
if (this.state === STATE.FULFILLED) {
onFulfilled(this.value)
}
if (this.state === STATE.REJECTED) {
onRejected(this.reason)
}
}
}
Copy the code
At this point a simple MyPromise is implemented, but at this point it can only handle synchronous tasks, not asynchronous operations
Asynchronous processing
To handle asynchronous operations, you can take advantage of the nature of queues by caching callback functions until the result of an asynchronous operation is returned.
In terms of concrete implementation, add judgment in then method. If the state is pending, write the incoming function to the corresponding callback function queue; Two arrays are used to hold queues of successful and failed callback functions respectively when the Promise is initialized, and they are augmented in the FULFILL and Reject callbacks. As follows:
class MyPromise {
constructor(fn) {
/ / initialization
this.state = STATE.PENDING
this.value = null
this.reason = null
// Save the array
this.fulfilledCallbacks = []
this.rejectedCallbacks = []
/ / success
const fulfill = (value) = > {
// State can be changed only if state is pending
if (this.state === STATE.PENDING) {
this.state = STATE.FULFILLED
this.value = value
this.fulfilledCallbacks.forEach(cb= > cb())
}
}
/ / fail
const reject = (reason) = > {
if (this.state === STATE.PENDING) {
this.state = STATE.REJECTED
this.reason = reason
this.rejectedCallbacks.forEach(cb= > cb())
}
}
Reject is called when there is an error executing the function
try {
fn(fulfill, reject)
} catch (e) {
reject(e)
}
}
then(onFulfilled, onRejected) {
if (this.state === STATE.FULFILLED) {
onFulfilled(this.value)
}
if (this.state === STATE.REJECTED) {
onRejected(this.reason)
}
// When THEN is pending, write the two states to the array
if (this.state === STATE.PENDING) {
this.fulfilledCallbacks.push((a)= > {
onFulfilled(this.value)
})
this.rejectedCallbacks.push((a)= > {
onRejected(this.reason)
})
}
}
}
Copy the code
Chain calls
The next step is to modify MyPromise to support chained calls. If you have used jquery and other libraries, you should be familiar with chained calls. The principle is that the caller returns itself, in this case the then method returns a promise. There is also the passing of return values:
class MyPromise {
constructor(fn) {
/ /...
}
then(onFulfilled, onRejected) {
return new MyPromise((fulfill, reject) = > {
if (this.state === STATE.FULFILLED) {
// Pass the return value to the next FULFILL
fulfill(onFulfilled(this.value))
}
if (this.state === STATE.REJECTED) {
// Pass the return value into the next reject
reject(onRejected(this.reason))
}
// When THEN is pending, write the two states to the array
if (this.state === STATE.PENDING) {
this.fulfilledCallbacks.push((a)= > {
fulfill(onFulfilled(this.value))
})
this.rejectedCallbacks.push((a)= > {
reject(onRejected(this.reason))
})
}
})
}
}
Copy the code
At this point, MyPromise already supports asynchronous operations, chain calls, and passing return values, which is a simplified version of a promise. Generally, when you need to write a promise by hand during an interview, this is enough. A full implementation of the Promise /A+ specification is also unrealistic in such A short time frame as an interview.
The full code up to this point is available in promise3.js
Promise/A + specification
OnFulfilled /onRejected returns A value X, which needs to be processed as follows:
- If x is equal to the promise returned by the then method, throw one
TypeError
error - If x is one
Promise
Keep the value of the promise returned by the then method consistent with the value of x - If x is an object or function, then
x.then
Assigned tothen
And call the- if
then
Is a function, x is the scopethis
Call, passing two argumentsresolvePromise
和rejectPromise
If theresolvePromise
和rejectPromise
Are called or are called multiple times, the first call is adopted and the remaining calls are ignored - If the call
then
If the method fails, the thrown error E is used as the rejection reason to reject the promise - if
then
If it is not a function, then the promise is executed with an x argument
- if
- If x is any other value, the promise is executed with x as an argument
Next, the MyPromise implemented in the previous step is further optimized to conform to the Promise /A+ specification:
class MyPromise {
constructor(fn) {
/ /...
}
then(onFulfilled, onRejected) {
const promise2 = new MyPromise((fulfill, reject) = > {
if (this.state === STATE.FULFILLED) {
try {
const x = onFulfilled(this.value)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
}
if (this.state === STATE.REJECTED) {
try {
const x = onRejected(this.reason)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
}
// When THEN is pending, write the two states to the array
if (this.state === STATE.PENDING) {
this.fulfilledCallbacks.push((a)= > {
try {
const x = onFulfilled(this.value)
generatePromise(promise2, x, fulfill, reject)
} catch(e) {
reject(e)
}
})
this.rejectedCallbacks.push((a)= > {
try {
const x = onRejected(this.reason)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
})
}
})
return promise2
}
}
Copy the code
Here we encapsulate the behavior of handling the return value x as a function generatePromise, which looks like this:
const generatePromise = (promise2, x, fulfill, reject) = > {
if (promise2 === x) {
return reject(new TypeError('Chaining cycle detected for promise'))}// If x is a promise, call its then method to continue the loop
if (x instanceof MyPromise) {
x.then((value) = > {
generatePromise(promise2, value, fulfill, reject)
}, (e) => {
reject(e)
})
} else if(x ! =null && (typeof x === 'object' || typeof x === 'function')) {
// To prevent repeated calls, success and failure can only be called once
let called;
// If x is an object or function
try {
const then = x.then
if (typeof then === 'function') {
then.call(x, (y) => {
if (called) return;
called = true;
// y is a promise
generatePromise(promise2, y, fulfill, reject)
}, (r) => {
if (called) return;
called = true;
reject(r)
})
} else {
fulfill(x)
}
} catch(e) {
if (called) return
called = true
reject(e)
}
} else {
fulfill(x)
}
}
Copy the code
Promise2 = promise1. Then (onFulfilled, onRejected)
- OnFulfilled /onRejected must be called asynchronously and cannot be synchronized
- If ondepressing is not a function and promise1 executes successfully, promise2 must execute successfully and return the same value
- If onRejected is not a function and promise1 rejects execution, promise2 must reject execution and return the same rejection
For the final improvement of then method, setTimeout is added to simulate asynchronous call, and the judgment of onFulfilled and onRejected methods is added:
class MyPromise {
constructor(fn) {
/ /...
}
then(onFulfilled, onRejected) {
// This is a pity and onRejected
onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value= > value
onRejected = typeof onRejected === 'function' ? onRejected : e= > { throw e }
const promise2 = new MyPromise((fulfill, reject) = > {
// setTimeout Macro task, ensure that onFulfilled and onRejected are executed asynchronously
if (this.state === STATE.FULFILLED) {
setTimeout((a)= > {
try {
const x = onFulfilled(this.value)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
}, 0)}if (this.state === STATE.REJECTED) {
setTimeout((a)= > {
try {
const x = onRejected(this.reason)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
}, 0)}// When THEN is pending, write the two states to the array
if (this.state === STATE.PENDING) {
this.fulfilledCallbacks.push((a)= > {
setTimeout((a)= > {
try {
const x = onFulfilled(this.value)
generatePromise(promise2, x, fulfill, reject)
} catch(e) {
reject(e)
}
}, 0)})this.rejectedCallbacks.push((a)= > {
setTimeout((a)= > {
try {
const x = onRejected(this.reason)
generatePromise(promise2, x, fulfill, reject)
} catch (e) {
reject(e)
}
}, 0)})}})return promise2
}
}
Copy the code
The complete promise code that implements the Promise /A+ specification is available as promise4.js
How do you know if the promise you implement follows the Promise /A+ specification? Promises – Aplus-Tests is an NPM package that can be used to do this
Other apis
Other commonly used Promise apis are implemented here
The catch, finally
class MyPromise {
constructor(fn) {
/ /...
}
then(onFulfilled, onRejected) {
/ /...
}
catch(onRejected) {
return this.then(null, onRejected)
}
finally(callback) {
return this.then(callback, callback)
}
}
Copy the code
Promise.resolve
Return a Promise object in the Resolved state
MyPromise.resolve = (value) = > {
// Pass in the promise type and return it directly
if (value instanceof MyPromise) return value
// When the thenable object is passed in, the then method is immediately executed
if(value ! = =null && typeof value === 'object') {
const then = value.then
if (then && typeof then === 'function') return new MyPromise(value.then)
}
return new MyPromise((resolve) = > {
resolve(value)
})
}
Copy the code
Promise.reject
Return a Promise object in the Rejected state
MyPromise.reject = (reason) = > {
// Pass in the promise type and return it directly
if (reason instanceof MyPromise) return reason
return new MyPromise((resolve, reject) = > {
reject(reason)
})
}
Copy the code
Promise.race
Returns a promise that changes as soon as a promise state in the iterator changes
MyPromise.race = (promises) = > {
return new MyPromise((resolve, reject) = > {
// Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
for(let promise of promises) {
// If the current value is not a Promise, use the resolve method to make a Promise
if (promise instanceof MyPromise) {
promise.then(resolve, reject)
} else {
MyPromise.resolve(promise).then(resolve, reject)
}
}
})
}
Copy the code
Promise.all
This is a big pity. The returned promise will become a big pity only when all the promises in the iterator become fulfilled. There will be a promise in the iterator rejected and the returned promise will become fulfilled
MyPromise.all = (promises) = > {
return new MyPromise((resolve, reject) = > {
const arr = []
// The number returned
let count = 0
// Current index
let index = 0
// Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
for(let promise of promises) {
// If the current value is not a Promise, use the resolve method to make a Promise
if(! (promiseinstanceof MyPromise)) {
promise = MyPromise.resolve(promise)
}
// Use closures to ensure that arrays are returned asynchronously
((i) = > {
promise.then((value) = > {
arr[i] = value
count += 1
if (count === promises.length || count === promises.size) {
resolve(arr)
}
}, reject)
})(index)
/ / the index increases
index += 1}})}Copy the code
Promise.allSettled
Only after all the promises in the iterator return, will a fulfilled promise be returned, and the returned promise state will always be fulfilled, and will not return the Rejected state
MyPromise.allSettled = (promises) = > {
return new MyPromise((resolve, reject) = > {
const arr = []
// The number returned
let count = 0
// Current index
let index = 0
// Promises are not arrays, but must have an Iterator interface, so use for... Of traversal
for(let promise of promises) {
// If the current value is not a Promise, use the resolve method to make a Promise
if(! (promiseinstanceof MyPromise)) {
promise = MyPromise.resolve(promise)
}
// Use closures to ensure that arrays are returned asynchronously
((i) = > {
promise.then((value) = > {
arr[i] = value
count += 1
if (count === promises.length || count === promises.size) {
resolve(arr)
}
}, (err) => {
arr[i] = err
count += 1
if (count === promises.length || count === promises.size) {
resolve(arr)
}
})
})(index)
/ / the index increases
index += 1}})}Copy the code
If there are any mistakes in this article, you are welcome to criticize
reference
- MDN-Promise
- ECMAScript 6 Getting Started -Promise objects
- 【 例 句 】Promises/A+ Promises
- Complete example and test code: github/ LVQQ/Promise