A, start
From about half a year ago, I tried to write a standard Promise, but I could not write it. During this period, I also read a lot of Promise articles, but usually I could not understand them after reading a little. In recent days, I have carefully studied again and looked up many articles, and finally fully understood Promise. The reason for writing this series of articles is that I think most of the articles about Promise implementation on the Internet are a little deep. I couldn’t understand them when I read them before. It’s not that the writing is bad, but many friends who are still learning can’t understand them, so I decided to try my best. Try to write a Promise implementation that can be understood by most front-end partners, as long as you have experience with the use of promises.
These three articles will help you build A Promise that fully conforms to the Promise A+ specification, and pass the 872 test cases provided by the government. I’ll break down all the knowledge and tips I need to make a Promise. Here we go!
2. Understand the general form of Promise through its use
For the implementation of promises, let’s ignore the specification and take a look at how it works. Take Promise, which is supported natively in Chrome.
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > { // Simulate an asynchronous execution
resolve(1)},1000)
})
promise1.then(function(res) {
console.log(res)
}, function(err){
console.log(err)
})
Copy the code
This is the code we often write when we use promises, and from this code we get the following information:
- Promise is a constructor that can be new
- It takes a function, which we’ll call executor, as an argument when it constructs an instance, new
- Promise has an instance method called THEN
From these conditions, we can make the following implementation of our own MyPromise:
function MyPromise(executor) {
}
MyPromise.prototype.then = function() {}Copy the code
Next, let’s take a closer look at the arguments passed when constructing a Promise, the function I called executor above, and its two arguments
Executor, the parameter passed during instantiation
From the general code that uses promises above, we know that executor is a function, so one thing is clear: executor concrete code is written by the consumer and invoked from within the Promise. Something like this:
function MyPromise(executor) {
executor()
}
MyPromise.prototype.then = function() {}/ / use MyPromise
// Executor is written by someone who uses MyPromise
function executor(resolve, reject) {
setTimeout((a)= > {
resolve(1)},1000)}let promise1 = new MyPromise(executor)
Copy the code
As a rule of thumb, the user writes executor with two parameters, resolve and reject, and calls resolve and reject when appropriate, so resolve and reject are functions that are implemented inside promises. So, the MyPromise we implement should contain the implementation of the resolve and Reject methods and be passed to executor as arguments when called.
function MyPromise(executor) {
let resolve = function() {} // Resolve and reject are optional
let reject = function() {}
executor(resolve, reject) // Just pass it when called
}
MyPromise.prototype.then = function() {}/ / use the Promise
let promise1 = new MyPromise(function(resolve, reject) {
setTimeout((a)= > {
resolve(1) // Resolve or reject is called by the consumer
}, 1000)})Copy the code
In fact, the resolve and reject functions implemented inside MyPromise don’t have to be called resolve or Reject. You can call a, B, or even a dog, as long as you pass them to the Executor when it executes. Because of this, the user can use the executor parameter when writing the executor concrete.
So the resolve and reject functions are implemented by us, who implement the MyPromise, and called by the person who uses the MyPromise. It is important to clarify this point.
Now, our code to implement Promise looks like this:
function MyPromise(executor) {
let resolve = function() {}
let reject = function() {}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}Copy the code
Next, the focus of this section is to clarify the functionality and implementation of the resolve and Reject functions in MyPromise
Implement resolve and reject functions
MyPromise now has only one shelf, and you must complete the resolve and reject functions. So what is Resovle and Reject? Here, some of the Promise A+ specification has to be mentioned.
According to the specification, an instance of a Promise may have three states:
pending
pendingfulfilled
The successful staterejected
The rejection state can also be understood as the failed state
The reason for these three states is that we typically use promises for asynchronous operations, the results of which can succeed or fail depending on the situation.
A Promise is instantiated in a pending state by default. Who changes its state? The answer is resolved or reject. When resolve or Reject is called, resolve changes the Promise instance from its pending state to the fulfilled state, and Reject changes its pending state to the Rejected state. At this point, the first function of resolve and Reject becomes clear.
To do this, however, we define a state in our MyPromise and then change it in resolve and Reject
function MyPromise(executor) {
this.status = 'pending' // The state is pending by default
let resolve = function() {
// The resolve method will change the pending state to depressing
this.status = 'fulfilled'
}
let reject = function() {
The // reject method changes the state to Rejected
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}Copy the code
There are, however, problems with this. The resolve and reject functions that we declare in the MyPromise constructor are all window by default, not MyPromise instances. There are a lot of ways to solve this problem, you can save this, or you can just use the arrow function, which we’ll do in this case.
function MyPromise(executor) {
this.status = 'pending'
let resolve = (a)= > { // This points to the outside
this.status = 'fulfilled'
}
let reject = (a)= > { // This points to the outside
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}Copy the code
Another problem with the code above is that, according to the specification, if a Promise instance changes state, it is fixed and its state will never change again. That is, if a Promise instance changes from pending to fulfilled, it cannot change back to Pending or Rejected. But this one doesn’t work. You can paste the following code into the browser and run it, and you’ll find the problem.
function MyPromise(executor) {
this.status = 'pending' // The state is pending by default
let resolve = (a)= > {
this.status = 'fulfilled'
}
let reject = (a)= > {
this.status = 'rejected'
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}let promise1 = new MyPromise(function(resolve, reject) {
setTimeout((a)= > { // Don't forget it, because it is only possible to print promise1 instances asynchronously
resolve(1)
console.log(promise1) // Here is {status: 'depressing '} successful state
reject(1)
console.log(promise1) // If you are in the same state, you are in the same state
}, 1000)})Copy the code
When resolve is run, check whether this.status is pending. If so, change it. If not, do nothing. The same is true for reject, so a call to resolve or reject is ignored when a promise’s STAus state changes.
function MyPromise(executor) {
this.status = 'pending'
let resolve = (a)= > {
// Check whether the state is pending. If so, change it. If not, do nothing
if (this.status === 'pending') {
this.status = 'fulfilled'}}let reject = (a)= > {
// If ()
if (this.status === 'pending') {
this.status = 'rejected'
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}let promise1 = new MyPromise(function(resolve, reject) {
setTimeout((a)= > { // Don't forget it, because only in asynchrony can you print an instance of promise1
resolve(1)
console.log(promise1) // Here is {status: 'depressing '} successful state
reject(1) // It is rejected, but it is ignored
console.log(promise1) // Still successful up to here
}, 1000)})Copy the code
That completes the first function of resolve and Reject.
Next, we implement the second function of Resolve and Reject.
Those of you who have used Promises already know that we pass a value to resolve or Reject that is associated with the implementation of the THEN method. Let’s take a look at an example of Chrome using Promise:
let promise1 = new Promise(function(resolve, reject) {
setTimeout((a)= > { // Simulate an asynchronous execution
let flag = Math.random() > 0.5 ? true: false
if (flag) {
resolve('success') // Pass a value
} else {
reject('fail') // Pass a value}},1000)
})
promise1.then(function(res) { // The value passed by the resolve call will be retrieved by this function
console.log(res)
}, function(err) { // Reject is taken here
console.log(err)
})
Copy the code
From this example, we can see that the values passed in resolve and reject are taken as arguments by the two functions passed in then. Here we know that the values passed in resolve and reject must be stored, and that the two functions passed in then get them at some point and execute them.
So a second function of resolve and reject comes into play: it stores the values of the call, which are then used by the two functions passed in the then method.
Because they are called on success and failure, we need to store them separately. To do this, MyPromise needs to add two attributes to the constructor and assign them when the resolve and Reject functions are executed.
MyPromise is written as follows:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined // Store the value passed to resolve
this.reason = undefined // Stores the value passed in reject
// Add parameters, because the consumer usually passes parameters when calling
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
}
}
Reject indicates failure. Therefore, write failure reason
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function() {}Copy the code
In fact, the values passed by resolve and reject can be placed in a single attribute, because once the promise instance state is changed, only one of them can be executed, and any subsequent execution will be ignored by the if condition. However, for the sake of correspondence, we use two attributes to hold the values passed by resolve and reject respectively.
Resolve and Reject implement two functions, respectively. The brothers actually have three functions each, but the third function is closely related to the then method, so the third function needs to be written with the THEN method. But first, let’s talk about the order in which Promises handle asynchronous code.
The order in which promises are executed when handling asynchrony
Let’s take a look at the execution order of asynchronous code handled by native Promise support in Chrome using console.log(). Take a closer look at the following example:
let promise1 = new Promise(function(resolve, reject) {
console.log(1)
setTimeout((a)= > { // Simulate an asynchronous execution
let flag = Math.random() > 0.5 ? true: false
if (flag) {
resolve('success')
console.log(2) // Note that both reject and reject print 2
} else {
reject('fail')
console.log(2)}},1000)})console.log(3)
promise1.then(function(res) {
console.log(res)
}, function(err) {
console.log(err)
})
console.log(4)
Copy the code
If you paste the above code into the browser, you will see that it prints 1, 3, 4, 2 success or fail. Let’s trace the order:
- when
new Promise
The function passed to the constructor is executed, so print 1 first - then
setTimeout
The code needs to wait for the next execution sequence, and then the construction ends, and the constructed instance is assigned to the variablepromise1
console.log(3)
Run to print 3promise1.then()
To perform,However, neither function passed in the THEN method was executed, which would have printed success or failconsole.log(4)
Run to print 4. End of current sequence- The next execution sequence starts, and the code in setTimeout that was used when the promise1 was constructed starts executing
- According to the condition
resolve
orreject
Execute, and thenconsole.log(2)
Go to print 2 - Finally, the two functions passed in the then() method execute according to the condition, take the value previously passed in resolve or reject, and execute, printing success or fail
To recap: When Promise uses resolve and Reject to process asynchronous code, then executes before resolve or Reject, but the two functions passed by then do not execute until resolve or Reject executes. This is actually a design pattern: the distribution and subscription pattern, also known as the observer pattern.
This summary doesn’t matter if you don’t understand it, because it’s coming up.
Distribute – subscribe and then
The distribution-subscribe pattern, also known as the observer pattern, is so prevalent on the front end that you can see it in almost every event mechanism and asynchronous processing. Let’s start with an example:
let app = document.getElementById('app')
app.addEventListener('click'.function fn1() {
console.log(1)
})
app.addEventListener('click'.function fn2() {
console.log(2)
})
app.addEventListener('click'.function fn3() {
console.log(3)})Copy the code
Fn1, Fn2 and FN3 will only be executed when clicking the TAB with the ID “app”. When the code is executed to app.addeventListener, the corresponding function is not executed, but is not executed until the click. So, as you can guess, FN1, FN2, and FN3 have to be stored somewhere to be executed at once when certain conditions occur.
If you use VUE, watch in VUE works the same way: first register a function or some functions in a place, and when a state changes, execute the stored functions all at once.
The then in Promise does the same thing. When a Promise handles asynchrony, the then method executes first, registering the two functions as parameters separately in the instance. Wait until resolve or reject is called.
This is where the then method and the third functionality of resolve and Reject come in.
- First, define two arrays in the constructor, resolvedCallbacks and rejectedCallbacks, to hold the two functions passed in by the THEN method
- The then method takes two parameters, a successful callback and a failed callback, named onResolved and onRejected
- Then push the onResolved function to the resolvedCallbacks and the onRejected function to the rejectedCallbacks
- ResolvedCallbacks (resolve, resolvedCallbacks, resolvedCallbacks, resolvedCallbacks
this.data
Is passed to them; So is Reject
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = [] // Stores the first argument passed in by the then method, a successful callback
this.rejectedCallbacks = [] // Stores the second argument passed in by the then method, the failed callback
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
// Execute all successful callbacks and pass this.data
this.resolvedCallbacks.forEach(fn= > fn(this.data))
}
}
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
// Execute all failed callbacks and pass this.reason
this.rejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
executor(resolve, reject)
}
// The then method receives parameters named onResolved and onRejected
MyPromise.prototype.then = function(onResolved, onRejected) {
this.resolvedCallbacks.push(onResolved) // Save onResolved
this.rejectedCallbacks.push(onRejected) // Save onRejected
}
Copy the code
Note that all of this code is based on processing asynchronous code, where then methods are executed before resolve or reject. Therefore, we need to make another judgment in then, that is, when the current promise is pending, we need to store the callback push in the corresponding place.
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn= > fn(this.data))
}
}
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function(onResolved, onRejected) {
// Check the status, execute only when pending
if (this.status === 'pending') {
this.resolvedCallbacks.push(onResolved)
this.rejectedCallbacks.push(onRejected)
}
}
Copy the code
If you look at this step, you might wonder why an array is used for functions passed by the then method. Because Promise can be used like this
let promise = new Promise(function(resolve, reject){
setTimeout((a)= > {
resolve(1)},1000)
})
promise.then(function(res) {
console.log('processing res')
})
promise.then(function(res) {
console.log('One more time')})Copy the code
This example then twice on the same Promise instance, registering the function twice. When resolve is executed, both functions registered with THEN are executed.
Also, you might ask, now that all of our promises deal with asynchronous cases, what about synchronous cases? Well, that’s what I’m going to say.
Handle synchronization
We usually use promises to deal with asynchrony, and our MyPromise is still based on the premise of dealing with asynchrony. In fact, promises can handle synchronization as well, and it’s pretty simple.
If you remember the Promise execution sequence, asynchronous then methods are executed before resolve or reject, and synchronous then methods are executed after resolve or reject. Look at the following example:
let promise = new Promise(function(resolve, reject){
resolve('success')
})
promise.then(function(res) {
console.log(res)
})
Copy the code
Resolve (resolve); resolve (resolve); then (fail);
MyPromise makes the following changes:
function MyPromise(executor) {
this.status = 'pending'
this.data = undefined
this.reason = undefined
this.resolvedCallbacks = []
this.rejectedCallbacks = []
let resolve = (value) = > {
if (this.status === 'pending') {
this.status = 'fulfilled'
this.data = value
this.resolvedCallbacks.forEach(fn= > fn(this.data))
}
}
let reject = (reason) = > {
if (this.status === 'pending') {
this.status = 'rejected'
this.reason = reason
this.rejectedCallbacks.forEach(fn= > fn(this.reason))
}
}
executor(resolve, reject)
}
MyPromise.prototype.then = function(onResolved, onRejected) {
if (this.status === 'pending') {
this.resolvedCallbacks.push(onResolved)
this.rejectedCallbacks.push(onRejected)
}
// The callback function succeeds directly if the status is successful
if (this.status === 'fulfilled') {
onResolved(this.data)
}
// Call the failed callback function
if (this.status === 'rejected') {
onRejected(this.reason)
}
}
Copy the code
We can test it out
let promise = new MyPromise(function(resolve, reject) {
setTimeout((a)= > {
let flag = Math.random() > 0.5 ? true : false
if (flag) {
resolve('success')}else {
reject('fail')}},1000)
})
promise.then(res= > {
console.log(res)
}, error => {
console.log(error)
})
Copy the code
Here, the prototype of MyPromise is complete! Well, it’s a prototype, the core then method that we haven’t implemented very much yet. However, if you can read all 40 lines of code, you’re on the way!
In the next article, we need to complete the implementation of the most core THEN method! Make a Promise with you