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:

  • pendingpending
  • fulfilledThe successful state
  • rejectedThe 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:

  • whennew PromiseThe function passed to the constructor is executed, so print 1 first
  • thensetTimeoutThe 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 3
  • promise1.then()To perform,However, neither function passed in the THEN method was executed, which would have printed success or fail
  • console.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 conditionresolveorrejectExecute, 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, resolvedCallbacksthis.dataIs 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