This article will lead you to implement a simple Promise. Before reading this article, by default, you have a basic understanding of Promise, basic use, and a certain understanding of ES6 syntax
1. Promise constructor
First, let's start with Promise's constructor, which is the most critical step in using Promise
// Create three constants to represent the three states of Promise const PENDING = 'pending' const FULFILLED = 'fulfilled' const REJECTED = 'rejected' // Promise class class Promise { // The Promise constructor passes in a method constructor(executor) { // Promise has a variable to save the status and save the status results this.promiseState = PENDING this.promiseResult = null // Create two functions to be passed in by Promise to change the state to success and failure const resolve = res => { // We use the arrow function to bind this // The successful function is used to change the Promise status to full and save the success value // We know that if the Promise state has changed, it can no longer be changed, so we need to do a test if (this.promiseState !== PENDING) return this.promiseState = FULFILLED this.promiseResult = res } const reject = err => { if (this.promiseState !== PENDING) return this.promiseState = REJECTED this.promiseResult = err } // Finally, we need to call the executor, pass in two state changing functions, and handle exceptions try { executor(resolve, reject) } catche (err) { // If an exception occurs, we call the reject function to change the Promise state to failed reject(err) } } }
Above, we have implemented a basic constructor. However, the above can only deal with the situation that the state of the executor changes synchronously. If it is an asynchronous call of resolve or reject, it will not work. We will improve this function later when writing the then function
2. Static method resolve
Because we will use this method in the then function, we write this function first
Before writing, we need to know the simultaneous interpreting of Promise.resolve functions for different incoming parameters. This function is used to wrap a value called Promise object, but it will be processed differently according to different parameter values.
1. The passed in is a Promise object
If a Promise object is passed in, a Promise object will be returned idempotent, which actually means to return the Promise object itself
2. What is passed in is a thenable object
The meaning of thenable object is that there is a then method in the object. At this time, this then method has the same function as the executor passed in by the Promise constructor, that is, the then method will receive the resolve and reject methods in the parameters respectively, which can be used to change the state of the Promise object
3. Pass in other values
If it is not the above two types, Promise will create a new Promise object, change the state of the new Promise to fully, and set the state result to the incoming value, that is, Promise will wrap the value
With the above analysis, let's write about this method
class Promise { static resolve(wrapped) { if (wrapped instanceof Promise) { // If it is Promise object return wrapped } else if (wrapped instanceof Object && wrapped.then instanceof Function) { // Is a thenable object, and the then method is equivalent to the executor function return new Promise(wrapped.then) } else { // Other values are packaged directly return new Promise(resolve => resolve(wrapped)) } } }
Of course, if the above code wants to run normally, it must be after our Promise implementation is completed. Because Promise is used in the code, we write this function first to facilitate the writing of then function
3. Static method reject
Since we have all written resolve, let's write it together with reject. Reject is not like the resolve function. The resolve function is idempotent, but the reject function will wrap the value into a rejected Promise object no matter what value it receives. Without saying much, write the code directly
class Promise { static reject(wrapped) { return new Promise((resolve, reject) => reject(wrapped)) } }
It's done in one line of code. It's very simple, isn't it
4. then method, the core of Promise
The above are just appetizers. The then method is our top priority. Before writing the then method, let's sort out some rules in the then method
1. First, the then method can pass in two methods. The first is a callback to resolve execution, and the second is a callback to reject execution
2. The then method returns a Promise object. The status of the returned Promise object is determined according to the return value of the resolved or rejected callback function
3. Resolving or rejecting an exception in a function will cause the returned Promise object to be rejected
4. Value passing and exception penetration (explained later)
Next, we will reflect these functions in the code in the form of code comments, including some asynchronous state change processing, which will be explained in the code comments
class Promise { // The first is the incoming parameters, a total of two, which implements the first rule then(onFulfilled, onRejected) { // A new Promise object is created here as the return value of the function // The state of the Promise object is determined by the return values of the two handler functions of the parameter // We return this value at the end of the function, which implements the second rule above const resultPromise = new Promise((resolve, reject) => { // The value transfer of the third rule is realized here // If the callback function parameter passed in is not a function // Then the status and value of the returned Promise object need to synchronize the unprocessed value // So that the value can be passed to the callback of the resolved then method of the returned Promise object if (typeof onFulfilled !== 'function ') { onFulfilled = resolve } // Implementing exception penetration is the same as value passing // You need to pass the exception to the callback of the next then function if (typeof onRejected !== 'function ') { onRejected = reject } // The next step is to call the two callback functions if (this.promiseState === PENDING) { // It is suggested that you first take a look at the following handling of the resolution and rejection status, and then look at the waiting status // If the then method is executed and the state is still waiting, it indicates that the state changes asynchronously // At this point, we need to save the successful and failed callbacks and call them when the state changes // We can create an additional array in the constructor to store the callback function to be executed asynchronously // The code that needs to be changed in the constructor will be written after the then method this.promiseCallbacks.push({ // This onFulfilled is only the key name of the object. Don't confuse it with the onFulfilled passed in by then onFulfilled: () => { let value try { // What is called here is onFulfilled passed in by then value = onFulfilled(this.promiseResult) } catch (err) { // If an exception occurs, the status of the returned Promise is synchronized to reject reject(err) } Promise.resolve(value).then(resolve, reject) }, onRejected: () => { let value try { // What is called here is onRejected passed in by then value = onRejected(this.promiseResult) } catch (err) { // If an exception occurs, the status of the returned Promise is synchronized to reject reject(err) } Promise.resolve(value).then(resolve, reject) } }) } else if (this.promiseState === Promise.FULFILLED) { // If the status is resolved when executing the callback in the then method, execute the callback directly // Of course, executing the callback requires exception handling and synchronization of returning Promise status let value try { value = onFulfilled(this.promiseResult) } catch (err) { // If an exception occurs, the status of the returned Promise is synchronized to reject reject(err) } // Here, we skillfully use the static method resolve to synchronize the returned Promise value and state // The processing of the return value of the callback function passed in by then is the same as that of resolve // Therefore, we can wrap the return value with resolve, which will return the wrapped Promise object in each case // Then we just need to use the then method, and then pass the resolve and reject of the returned Promise object into then // Then the state of the returned Promise object can be synchronized with the return value of the then method callback function Promise.resolve(value).then(resolve, reject) } else if (this.promiseState === Promise.REJECTED) { // If the status is rejected, it is the same as above, except that the callback function called is different let value try { value = onRejected(this.promiseResult) } catch (err) { reject(err) } Promise.resolve(value).then(resolve, reject) } }) return resultPromise } // Here is the code of the constructor after the above changes. We have deleted unnecessary comments constructor(executor) { this.promiseState = PENDING this.promiseResult = null // Here we add an array of callbacks to save callback functions // You may wonder why you want to save with an array // Because a Promise object can call the then method multiple times, multiple callback functions can be saved this.promiseCallbacks = [] const resolve = res => { if (this.promiseState !== PENDING) return this.promiseState = FULFILLED this.promiseResult = res // Successful processing should call all successful callback functions this.promiseCallbakcs.forEach(item => item.onFulfilled()) } const reject = err => { if (this.promiseState !== PENDING) return this.promiseState = REJECTED this.promiseResult = err // Failed processing should call all failed callback functions this.promiseCallbakcs.forEach(item => item.onRejected()) } try { executor(resolve, reject) } catche (err) { reject(err) } } }
After looking at the above code, we may find that there are codes that need to be repeated. For example, there are very high similarities in handling exceptions and synchronization states, so we optimize the code and encapsulate the repeated places
// First, we also delete unnecessary comments and change the code that can be written in a single line to a single line class Promise { then(onFulfilled, onRejected) { const resultPromise = new Promise((resolve, reject) => { // Here we write a function that integrates exception handling and return value synchronization // Parameter handler is the function to be executed by the code, such as onFulfilled or onRejected const resultHandler = handler => { // A setTimeout is used here to simulate the asynchronous execution of the then method callback function // In Promise, the callback function of the then method is an asynchronous micro task. Let's use this to simulate asynchronous setTimeout(() => { // The structure is the same as before // Just replace the function to be called with the incoming handler let value try { value = handler(this.promiseResult) } catch (err) { reject(err) } // Here we add a circular reference detection // Prevent the then callback function from returning its own Promise object if (callbackReturn === returnPromise) { throw new TypeError('Chaining cycle detected for promise #<Promise>') } Promise.resolve(value).then(resolve, reject) }) } if (typeof onFulfilled !== 'function ') onFulfilled = resolve if (typeof onRejected !== 'function ') onRejected = reject if (this.promiseState === PENDING) { this.promiseCallbacks.push({ // The following places will become very simple. You can only pass in the corresponding processing function onFulfilled: () => resultHandler(onFulfilled) onRejected: () => resultHandler(onRejected) }) } else if (this.promiseState === Promise.FULFILLED) { resultHandler(onFulfilled) } else if (this.promiseState === Promise.REJECTED) { resultHandler(onRejected) } }) return resultPromise } }
Here, it's not difficult to find out. As long as you have a certain understanding of Promise function, I believe you can summarize it yourself
5. catch method
With the above function, the rest of the methods will become very easy to implement
class Promise { catch(onRejected) { // The catch method is actually used to handle exceptions that are penetrated by exceptions, so it has the same function as onRejected in the then method // So we can directly use the then method to complete its function return this.then(null, onRejected) } }
6. finally method
class Promise { finally(onFilnally) { // finally, the same is true, but the same method is called whether it is resolved or rejected // However, the finally callback function does not need to pass in a value, so wrap it with an arrow function return this.then(() => onFilnally(), () => onFilnally()) } }
7. Static method
The all method can receive an array. If the value is a Promise object, the all method will return a solution value only when all objects are in the solution state. If any Promsie object is rejected, the Promise returned by the all method is also rejected, and the reason for rejection is the reason for the rejected Promise, If some elements in the parameter array are not Promise objects, they will be wrapped as Promise objects
class Promise { static all(promises) { // By default, we pass in an array, eliminating some checking steps return new Promise((resolve, reject) => { // Array to place results let ret = [] // Used to determine whether all promise objects in the array have been processed let count = 0 // We should go through the array to process each Promise object in the array for (let index = 0; index < promises.length; ++index) { // We wrap the value with Promise.resolve so that all elements are called Promise objects Promise.resolve(promises[index]).then( res => { // If successful, store its success value count++ ret[index] = res if (count === promises.length) { // If all are resolved, the returned Promise is resolved resolve(ret) } }, err => { // If any one fails, the returned Promise is failed reject(err) } ) } }) } }
8. Static method race
race method is simpler than all. Whoever is fast is who
class Promise { static race(promises) { // By default, we pass in an array, eliminating some checking steps return new Promise((resolve, reject) => { for (let index = 0; index < promises.length; ++index) { Promise.resolve(promises[index]).then( res => { resolve(res) }, err => { reject(err) } ) } }) } }
9. Organize all codes
The integration of all codes is listed below, and the comments are deleted
class Promise { constructor(executor) { this.promiseState = PENDING this.promiseResult = null this.promiseCallbacks = [] const resolve = res => { if (this.promiseState !== PENDING) return this.promiseState = FULFILLED this.promiseResult = res this.promiseCallbakcs.forEach(item => item.onFulfilled()) } const reject = err => { if (this.promiseState !== PENDING) return this.promiseState = REJECTED this.promiseResult = err this.promiseCallbakcs.forEach(item => item.onRejected()) } try { executor(resolve, reject) } catche (err) { reject(err) } } then(onFulfilled, onRejected) { const resultPromise = new Promise((resolve, reject) => { const resultHandler = handler => { setTimeout(() => { let value try { value = handler(this.promiseResult) } catch (err) { reject(err) } if (callbackReturn === returnPromise) { throw new TypeError('Chaining cycle detected for promise #<Promise>') } Promise.resolve(value).then(resolve, reject) }) } if (typeof onFulfilled !== 'function ') onFulfilled = resolve if (typeof onRejected !== 'function ') onRejected = reject if (this.promiseState === PENDING) { this.promiseCallbacks.push({ onFulfilled: () => resultHandler(onFulfilled) onRejected: () => resultHandler(onRejected) }) } else if (this.promiseState === Promise.FULFILLED) { resultHandler(onFulfilled) } else if (this.promiseState === Promise.REJECTED) { resultHandler(onRejected) } }) return resultPromise } static resolve(wrapped) { if (wrapped instanceof Promise) { return wrapped } else if (wrapped instanceof Object && wrapped.then instanceof Function) { return new Promise(wrapped.then) } else { return new Promise(resolve => resolve(wrapped)) } } static reject(wrapped) { return new Promise((resolve, reject) => reject(wrapped)) } catch(onRejected) { return this.then(null, onRejected) } finally(onFilnally) { return this.then(() => onFilnally(), () => onFilnally()) } static all(promises) { return new Promise((resolve, reject) => { let ret = [] let count = 0 for (let index = 0; index < promises.length; ++index) { Promise.resolve(promises[index]).then(res => { count++ ret[index] = res if (count === promises.length) resolve(ret) }, err => { reject(err) }) } }) } static race(promises) { return new Promise((resolve, reject) => { for (let index = 0; index < promises.length; ++index) { Promise.resolve(promises[index]).then(res => { resolve(res) }, err => { reject(err) }) } }) } }
10. Summary
So far, we have implemented a simple version of Promise. I believe after you think and practice, you will find that it is not difficult. I also hope you can learn from the article. If there are any mistakes in the article, I hope you can give advice. Thank you.