Writing in the front
This article will take you through the process of breaking promises and implementing them step by step. But before reading, you need to be familiar with the usage. It may be easier to understand the article with the usage.
structure
Let’s look at the simple usage.
const promise = new Promise((resolve, reject) => {
setTimeout(() => {
resolve('success')
})
})
.then(value => { ... }, reason => { ... })
.catch(error => { ... })
Copy the code
The Promise constructor receives a callback, which is the executor described below. Its arguments, resolve and reject, are also functions that change the state of the Promise instance and its value. The then callback is executed when the state changes.
Note: It is not the then function that executes after the state changes, but rather the callback function in then that executes after the state changes. The then method queues its callbacks and executes the functions in the queue once the state of the promise changes.
If you were to implement the simplest promise class, what would the internal structure contain?
- Status: Fulfiled, Rejected, pending.
- Value: The value of the promise.
- Executor: Provides an entry point to change the promise state.
- Resolve and reject: the former changes the promise to fulfiled, and the latter changes the promise to rejected. Resolve or reject can be controlled within the actuator, depending on the actual business.
- Then method: Accept two callbacks, onFulfilled, onRejected. Execute after the promise state changes to Fulfiled or Rejected, respectively. This involves registering callbacks into the two execution queues, as described later.
const PENDING = 'pending'
const FULFILLED = 'fulfiled'
const REJECTED = 'rejected'class NewPromise { constructor(handler) { this.state = PENDING this.value = undefined this.successCallback = [] this.failureCallback = [] try { handler(this.resolve.bind(this), This.reject. Bind (this))} catch (e) {this. Reject (e)} // resolve and reject method resolve(value) {... } reject(reason) { ... } / /thenmethodsthen(onFulfilled, onRejected) { ... }}Copy the code
How is each part of the structure implemented?
The Promise executor
The executor is the callback that we pass in when we initialize a promise. It’s the entry point through which we manipulate the promise, so the implementation of the executor is not complicated, that is, it executes the callback that we pass in.
class NewPromise {
...
handler(resolve.bind(this), reject.bind(this))
...
}
Copy the code
In fact, the executor accepts two callbacks, resolve and reject. They really work to change the promise state.
Resolve and reject
There are actually two functions, passed as arguments to the actuator, that do uncomplicated things:
- Change the state of the promise
- Use the received value as the value of the promise
- The callbacks registered in THEN are executed in turn
const PENDING = 'pending'
const FULFILLED = 'fulfiled'
const REJECTED = 'rejected'Class NewPromise {constructor(handler) {this.state = PENDING this.value = undefined; This. SuccessCallback = [] this. FailureCallback = [] try {resolve and reject use thisbindBind (this), this.reject. Bind (this))} catch (e) {this.reject(e)}} resolve(value) {if(this.state ! == PENDING)returnThis. State = depressing this. Value = value // thissetTimeout Simulates asynchronous modesetTimeout(() => {
this.successCallback.forEach(item => {
item(value)
})
})
}
reject(reason) {
if(this.state ! == PENDING)return
this.state = REJECTED
this.value = reason
setTimeout(() => {
this.failureCallback.forEach(item => {
setTimeout(item(reason))
})
})
}
}
Copy the code
Take a look at their implementation, changing the state and assigning value. The most important point: loop through the then method to register the callback in the queue. The specification requires callbacks to be executed asynchronously to ensure that all callbacks are registered with THEN before they can be executed, so setTimeout is used to simulate this.
Then method:
(Translated from Promise/A+ specification)
A promise must provide then methods to access the current or final value of the promise. The then method has two parameters: onFulfilled and onRejected, both of which are optional. There are a few rules for these two parameters:
Ondepressing and onRejected are not functions and must be ignored
What it really means is that if it’s not a function, it’s assigned as a function by default, and returns the value of the promise that then belongs to. This is done so that the value of a promise can be passed if the then() function does not return a call. The scenario is as follows:
promise(resolve => resolve('success'))
.then()
.then(function(value) {
console.log(value)
})
Copy the code
In practice, you can assign a default function if it is not a function, or you can simply pass resolve or reject in the newly returned promise to achieve the effect of ignoring it.
Ondepressing is a function
- This must be called when the current promise state becomes depressing. The value of the promise will be resolved, which is also its first parameter
- Cannot be called before a pity
- It can be invoked at most once
OnRejected is a function
- Must be called when the state of the current Promise changes to Rejected. The promise rejected value is its first argument. Cannot be called before Rejected,
- It can be invoked at most once.
Then may be called multiple times
- When the promise state becomes a fulfilled, all onFulfilled will be invoked in the order that they were originally registered in the THEN method
- When the promise state changes to Rejected, all onRejected will be called in the order originally registered in the THEN method, like this:
const promise = new Promise((resolve, reject) => {
setTimeout(() => resolve('success'))
})
promise.then(res => {
console.log(res, 'First time');
})
promise.then(res => {
console.log(res, 'The second time');
})
Copy the code
In this case, we need to maintain two queues inside the promise we implemented. The elements in the queues are the callback functions registered in the THEN method (onFulfilled, onRejected). Each time the THEN is called, a callback will be registered to the queue. They are executed in sequence when the promise state changes.
Return a Promise for easy chain calls
promise2 = promise1.then(onFulfilled, onRejected);
Copy the code
The state of the promise (promise2) returned by THEN depends on the return value of its callback function (ondepressing or onRejected) or the state of promise1, which is expressed as follows:
- The return value of onFulfilled or onRejected is x, then the state of promise2 is resolve and the value is X
- If onFulfilled or onRejected fails and the error object E is thrown, then the state of promise2 is Rejected and the value of this error object E is fulfilled
- If ondepressing is not a function, but the state of promise1 becomes a pity, then the state of promise2 is also a pity and has the same value as promise1
- If onRejected is not a function and the state of promise1 changes to Rejected, then the state of promise2 is also rejected and the value is the same as that of promise1.
In accordance with these principles, I drew a diagram for the following
implementation
We have seen the then method above, and we can guess that the then method in our own promise implementation needs to do the following things:
- Return a new Promise instance
- The Promise to which THE THEN belongs is in the pending state, and the callback (onFulfilled,onRejected) of the THEN is put into the execution queue respectively for execution. The functions in the two queues can only be executed when the Promise state to which the THEN belongs is changed. This ensures the implementation timing of onFulfilled and onRejected in the specification.
- When the Promise state of then is not pending, the callbacks in the execution queue begin to execute sequentially, and then determine the state of the new Promise based on the changed state and the return value of the callback
- For example:
const promise1 = new Promise((resolve, reject) =>{ ... }) const promise2 = promise1.then(value => { return 'success' }, reason => { return 'failed' }) Copy the code
If the ondepressing callback is passed in then and the return value is success, then promise2 will be resolved with the value of success. Promise2 is rejected, because the callback of onRejected is passed in then and the value returned is failed, promise2 is rejected, reason is failed
Implement the then method step by step.
Class NewPromise {constructor(handler) {this.state = PENDING this.value = undefined; FailureCallback = [] Try {handler(this.resolve. Bind (this), this.reject.bind(this)) } catch (e) { this.reject(e) } }then(onFulfilled, onRejected) {
returnNew NewPromise((resolveNext, rejectNext) => {// Register a pengding state callback to the queueif(state === PENDING) {successCallback. Push (onFulfilled) failureCallback. Push (onFulfilled)} Use resolveNext or rejectNext to change the state of the new promiseif (state === FULFILLED) {
resolveNext(value)
}
if (state === REJECTED) {
rejectNext(value)
}
})
}
}
Copy the code
The above structure basically realizes the general logic of the THEN function, but does not realize the effect of the new promise state according to the implementation results of the onFulfilled and onRejected callbacks. They are only put into their respective execution queues.
The final then returns the promise state which is related to the implementation result of onFulfilled and onRejected. I compiled a chart based on the specification and the actual situation:
Then let’s implement this with code (just take the implementation of Ondepressing as an example).
Try {// Normalif(typeof onFulfilled ! = ='function') {// is not a functionthenBelong to the promise asthenResolveNext (value)}else{/ /thenConst res = ondepressing (value)if(res instanceof NewPromise) {// When the result of the execution returns a promise instance, wait for the promise state to changethenReturn the state of the promise res.then(resolveNext, rejectNext)}elseResolve resolveNext(res)}} catch (e) {resolve resolveNext(res)}} catch (e) {// The new promise status changes to Rejected, RejectNext (e)}Copy the code
The whole part needs to be queued to wait for the state of the PROMISE that then belongs to to change before execution, thus changing the state of the promise returned by THEN. So, we need to wrap this one up. Put it all together:
Class NewPromise {constructor(handler) {this.state = PENDING this.value = undefined; FailureCallback = [] Try {handler(this.resolve. Bind (this), this.reject.bind(this)) } catch (e) { this.reject(e) } }then(onFulfilled, onRejected) {
const { state, value } = this
returnNew NewPromise((resolveNext, rejectNext) => {const resolveNewPromise = value => {try {// Normal caseif(typeof onFulfilled ! = ='function') {// is not a functionthenResolve (value)} resolveNext(value)}else{/ /thenConst res = ondepressing (value)if(res instanceof NewPromise) {// When the result of the execution returns a promise instance, wait for the promise state to changethenReturn the state of the promise res.then(resolveNext, rejectNext)}elseResolve resolveNext(res)}} catch (e) {resolve resolveNext(res)}} catch (e) {// The new promise status changes to Rejected, RejectNext (e)}} const rejectNewPromise = reason => {try {// Normalif(typeof onRejected ! = ='function') {// is not a functionthenBelong to the promise asthenReject (reason)} Return the promise value reject (reject)else{/ /thenConst res = onRejected(reason)if(res instanceof NewPromise) {// When the result of the execution returns a promise instance, wait for the promise state to changethenReturn the state of the promise res.then(resolveNext, rejectNext)}elseReject rejectNext(res)}}} Catch (e) {reject rejectNext(res)}} catch (e) {reject rejectNext(res)}} catch (e) {reject reject rejectNext(res)}} RejectNext (e)}}if(state === PENDING) { this.successCallback.push(resolveNewPromise) this.failureCallback.push(rejectNewPromise) } // Be sure to change a new promise state after the current promise state changesif (state === FULFILLED) {
resolveNewPromise(value)
}
if (state === REJECTED) {
rejectNewPromise(value)
}
})
}
}
Copy the code
In our implementation, we define two arrays as task queues to store the then registered callbacks. In a real promise, the then method registers the callback to the microtask queue. Wait until the promise state changes before executing the tasks in the microtask queue. Microtasks can be conceptually considered asynchronous tasks, which supports the specification that callbacks to THEN must be executed asynchronously.
About the event loop some knowledge, I summarized an article, today, I understand the JS event loop mechanism
Catch the function
The catch function is used to handle exceptions and catch the error cause when the Promise state changes to Rejected.
This error can also be caught in the second callback of the THEN function (onRejected), assuming no catch is used. Catch returns a promise, so it is the same as calling promise.prototype. then(undefined, onRejected)
catch(onRejected) {
return this.then(undefined, onRejected)
}
Copy the code
Resolve method
Promise.resolve(value) returns a Promise object that wraps the passed value value as a Promise object.
So what’s the point of doing that? In fact, value may be an indeterminate value, it may or may not be a promise, and it may or may not call the then method. However, the resolve method can be used to unify the action on value. Such as:
const promise = function() {
if (shouldBePromise) {
return new Promise(function(resolve, reject) {
resolve('ok')})}return 'ok'
}
promise().then(() => {
...
})
Copy the code
The result that promise returns depends on shouldBePromise, if shouldBePromise is false, then promise returns the string OK, and we can’t call then.
Resolve can be wrapped in Promise(). Resolve, which always returns a Promise instance, ensuring that the then method is called.
Promise.resolve(promise()).then(() => {
...
})
Copy the code
To summarize the features: promise.resolve
- No, return a Promise in the Resolved state
- Is a Thenable object (that is, with the “then” method) that returns a Promise whose state changes when the object’s state changes, consistent with that object’s state
- Is a normal value that returns a Promise in the Resolved state with the normal value
- Is a Promise object, and returns that object
Static resolve(value) {// Resolve (value) {return resolved promiseif(! value) {return new NewPromise(function(resolve) {resolve()})} // if (resolve) {resolve()}) {// if (resolve) {resolve()})} // if (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {//thenmethodsif (value instanceof NewPromise) {
returnValue} // is the Thenable object, which returns a new Promise instance that needs to change after the value state changes, and the state follows the value stateif (typeof value === 'object' && typeof value.then === 'function') {
returnNew NewPromise((resolve, reject) => {value. Then (resolve, reject)})} // New NewPromise((resolve, reject) => {valuereturn new NewPromise(resolve => {
resolve(value)
})
}
Copy the code
Reject method
Reject is simpler than resolve. It always returns a Reject promise object, reject because of the reason we pass in
static reject(reason) {
return new NewPromise((resolve, reject) => {
reject(reason)
})
}
Copy the code
The finally method
This is a promise returned. When the promise ends, the callback function will be executed, no matter the result is fulfilled, which is a pity or Rejected. The state and value of the new promise returned depend on the original promise.
Finally (callback) {// The return value is a Promise objectthen, which conforms to the principle of invoking after the promise endsreturn this.then(
// thenBoth onFulfiled and onRejected are passed to the method, ensuring that both resolved and Rejected will be executed. Use this result as the value of the NewPromise returned res => newpromise.resolve (callback()).then(() => {returnRes}), // Get the result of an execution failure. Error => newpromise.resolve (callback()).then(() => {throw error}))}Copy the code
All methods
Promise.all(param) takes an array of parameters and returns a new Promise instance. A newly returned promise will be resolved only after all promises in the parameters array are resolved or instances in the parameters have been executed. If any promise in the array fails (Rejected), the newly returned promise fails because it is the result of the first failed promise.
const p1 = Promise.resolve(1),
coint p2 = Promise.resolve(2),
const p3 = Promise.resolve(3);
Promise.all([p1, p2, p3]).then(function(results) { console.log(results); // [1, 2, 3]});Copy the code
As you can see, the All method needs to return a new Promise instance and then control the state and value of the new Promise instance based on the execution of the received array of parameters
static all(instanceList) {
returnNew NewPromise((resolve, reject) => {const results = []let count = 0
if (instanceList.length === 0) {
resolve(results)
return} instancelist.foreach ((item, index) => {// Since each element in the list of instances can be various, Resolve (item). Then (res => {results[index] = res count++ // when all are finished, resolve returns a new promiseif(count === instancelist.length) {resolve(results)}}, error => { Reject new return promise reject(error)})})}Copy the code
Once implemented, it is clear that the all method executes all promises in parallel and outputs the results in the order of the promise array passed in. This reminds me of a previous interview topic: concurrent all requests and output them in order. This can be implemented with promise.all. But there will actually be requests that fail, so a better approach is promise.allSettled, as discussed below.
AllSettled method
If finally provides a way to execute code whether a single promise succeeds or not, Then allSettled provides the same way to handle multiple promise scenarios.
The promise.allSettled () method returns a Promise that is resolved after all given promises have been resolved or rejected, and each object describes the outcome of each Promise.
const promise1 = Promise.resolve(3);
const promise2 = new Promise((resolve, reject) => setTimeout(reject, 100, 'foo'));
const promises = [promise1, promise2];
Promise.allSettled(promises).
then((results) => results.forEach((result) => console.log(result.status)));
// expected output:
// "fulfilled"
// "rejected"
Copy the code
Unlike promise.all, once there is a failure to execute, there is no way to obtain the point in time when all promises have been executed. This point in time is obtained once all the promises are completed, regardless of whether a promise is successful or not in the allSettled method. Because new promises are returned, they are always resolved, and the value is a description of all promise execution results.
[{"status":"rejected"."reason":"Failure"},
{"status":"fulfiled"."value":"Success"}]Copy the code
To implement this, record the results in an array each time a promise is executed. Finally, resolve the array of results after all promises are executed, changing the state of any new promises returned.
static allSettled(instanceList) {
return new NewPromise((resolve, reject) => {
const results = []
let count = 0
if (instanceList.length === 0) {
resolve([])
return} // define a function to generate the result array const generateResult = (result, I) => {count++ results[I] = result // once all execution is complete, resolve to return a new promiseif(count === instanceList.length) { resolve(results) } } instanceList.forEach((item, Resolve (item). Then (value => {generateResult({status: FULFILLED, value }, index) }, reason => { generateResult({ status: REJECTED, reason }, index) } ) }) }) }Copy the code
Race method
Like the All method, it takes an array of instances as an argument and returns a new PROMISE. But the difference is that once a promise in the instance array succeeds or fails, the returned promise succeeds or fails.
static race(instanceList) {
return new NewPromise((resolve, reject) => {
if (instanceList.length === 0) {
resolve([])
return} instancelist.foreach (item => {// Since each element in the list of instances can be various, Resolve (item). Then (res => {// Once there is a resolve, Resolve resolve(res)}, error => {reject(error)})})})})}Copy the code
The complete code
So far a relatively complete promise has been implemented, with the following code:
class NewPromise { constructor(handler) { this.state = PENDING this.value = undefined this.successCallback = [] this.failureCallback = [] try { handler(this.resolve.bind(this), This. Reject. Bind (this))} catch (e) {reject. Reject (e)} resolve(value) {if(this.state ! == PENDING)returnThis. State = depressing this. Value = value // ThisthenThe callbacks registered in resolve are executed asynchronously, ensuring that all callbacks pass before resolve executes all callbacksthenRegistration completedsetTimeout(() => {
this.successCallback.forEach(item => {
item(value)
})
})
}
reject(reason) {
if(this.state ! == PENDING)return
this.state = REJECTED
this.value = reason
setTimeout(() => {
this.failureCallback.forEach(item => {
item(reason)
})
})
}
then(onFulfilled, onRejected) {
const { state, value } = this
returnNew NewPromise((resolveNext, rejectNext) => {const resolveNewPromise = value => {try {// Normal caseif(typeof onFulfilled ! = ='function') {// is not a functionthenBelong to the promise asthenResolveNext (value)}else{/ /thenConst res = ondepressing (value)if(res instanceof NewPromise) {// When the result of the execution returns a promise instance, wait for the promise state to changethenReturn the state of the promise res.then(resolveNext, rejectNext)}elseResolve resolveNext(res)}} catch (e) {resolve resolveNext(res)}} catch (e) {// The new promise status changes to Rejected, RejectNext (e)}} const rejectNewPromise = reason => {try {// Normalif(typeof onRejected ! = ='function') {// is not a functionthenBelong to the promise asthenReject (reason)} Return the promise value reject (reject)else{/ /thenConst res = onRejected(reason)if(res instanceof NewPromise) {// When the result of the execution returns a promise instance, wait for the promise state to changethenReturn the state of the promise res.then(resolveNext, rejectNext)}elseReject rejectNext(res)}}} Catch (e) {reject rejectNext(res)}} catch (e) {reject rejectNext(res)}} catch (e) {reject reject rejectNext(res)}} RejectNext (e)}}if(state === PENDING) { this.successCallback.push(resolveNewPromise) this.failureCallback.push(rejectNewPromise) } // Be sure to change a new promise state after the current promise state changesif (state === FULFILLED) {
resolveNewPromise(value)
}
if (state === REJECTED) {
rejectNewPromise(value)
}
})
}
catch(onRejected) {
returnThis. Then (undefined, onRejected)} finally(callback) {// Return a promise object, callbackthen, which conforms to the principle of invoking after the promise endsreturn this.then(
// thenBoth onFulfiled and onRejected are passed to the method, ensuring that both resolved and Rejected will be executed. Use this result as the value of the NewPromise that finally returns res => newpromise.resolve (callback()).then(() => {returnRes}), // Get the result of an execution failure. Error => newpromise.resolve (callback()).then(() => {throw error}))} static allfirst (instanceList) {error => newPromise.resolve (callback()).return new NewPromise((resolve, reject) => {
const results = []
let count = 0
if (instanceList.length === 0) {
resolve([])
return} // define a function to generate the result array const generateResult = (result, I) => {count++ results[I] = result // once all execution is complete, resolve to return a new promiseif(count === instanceList.length) { resolve(results) } } instanceList.forEach((item, Resolve (item). Then (value => {generateResult({status: FULFILLED, value }, index) }, reason => { generateResult({ status: Static resolve(value) {// Resolve (value)if(! value) {return new NewPromise(function(resolve) {resolve()})} // if (resolve) {resolve()}) {// if (resolve) {resolve()})} // if (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {// If (resolve) {//thenmethodsif (value instanceof NewPromise) {
returnValue} // is the Thenable object, which returns a new Promise instance that needs to change after the value state changes, and the state follows the value stateif (typeof value === 'object' && typeof value.then === 'function') {
returnNew NewPromise((resolve, reject) => {value. Then (resolve, reject)})} // New NewPromise((resolve, reject) => {valuereturn new NewPromise(resolve => {
resolve(value)
})
}
static reject(reason) {
return new NewPromise((resolve, reject) => {
reject(reason)
})
}
static all(instanceList) {
returnNew NewPromise((resolve, reject) => {const results = []let count = 0
if (instanceList.length === 0) {
resolve(results)
return} instancelist.foreach ((item, index) => {// Since each element in the list of instances can be various, Resolve (item). Then (res => {results[index] = res count++ // when all are finished, resolve returns a new promiseif(count === instancelist.length) {resolve(results)}}, error => { Reject new return promise reject(error)})})} static race(instanceList) {return new NewPromise((resolve, reject) => {
if (instanceList.length === 0) {
resolve([])
return
}
instanceList.forEach(item => {
// 由于实例列表中的每个元素可能是各种各样的,所以要用this.resolve方法包装一层
this.resolve(item).then(res => {
// 一旦有一个resolve了,那么新返回的promise状态就被resolve
resolve(res)
}, error => {
reject(error)
})
})
})
}
}
Copy the code
Until the end of advertising
For more technical articles I write, follow the public account: one front end at a time