Further understand and implement a simple Promise

Posted by roice on Tue, 07 Sep 2021 22:51:22 +0200

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.

Topics: Javascript