Give me time for an article that will walk you through the details of handwritten promises.

You may have seen many handwritten Promise articles, but most of them are based on questions about how to write promises. The author, as a Promise, has been relatively mature to enter the front of the industry, and is ready to explore with you how to hand write a Promise.

Before we begin, let’s recall what capabilities Promise had that made it so common. Start with the most commonly used scenario, something like this 🌰

const p=new MyPromise((resolve,reject) = >{
  setTimeout(() = >{
    resolve(1)},2000)  
})
p.then(v= >{
  console.log(v)
  return 2
})
.then(v= >{
  console.log(v)
})
// we expect to output 1,2 after 2s delay


const p=new MyPromise((resolve,reject) = >{
  setTimeout(() = >{
    reject(1)},2000)  
})
p.then(v= >{
  console.log(v)
  return 2
},r= >{
  console.log(v)
  return 3
})
.then(v= >{
  console.log(v)
})
// we expect to output 1,3 after 2s delay
Copy the code

Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+

features

  1. Status: The value must bepending fulfilled rejectedOne can only be made bypendingtofulfilledorrejectedAnd irreversible
  2. Need to provide athenMethod,
    1. Take two parameters
    2. onFulfilledCorresponding status changes tofulfilledWhen the callback;onRejectedCorresponding status changes torejectedWhen the callback
    3. Then can be called multiple times for the same promise
    4. Then needs to return a promise
  3. Provides a catch method, the result of which is the same as then(,fn)
  4. Static methods resolve, reject, All, and race

Based on the features listed above, we start with a simple analysis of the example 👇🏻

const p=new MyPromise((resolve,reject) = >{
  setTimeout(() = >{
    resolve(1)},1000)
})
p.then((v) = >{
  console.log(v)
},(rej) = >{
  console.error(rej)
})
Copy the code

This article uses the noun declaration

  1. Fn represents the function passed in to the MyPromise constructor
  2. A successful callback indicates the callback to be executed when MyPromise changes from Pending to fulfilled
  3. The failed callback represents the callback executed when MyPromise changes from Pending to Rejected

In the example, we use setTimeout to do an asynchronous task. In a real business scenario, this would be replaced by interface requests and so on. From the perspective of JS execution, these lines of code are roughly as follows:

  1. New the MyPromise constructor, which returns an object p
  2. The constructor takes a function fn, which itself takes resolve and reject, and calls either resolve or Reject, depending on the business. Both resolve and Reject support parameter passing
  3. The returned object P has a then method that takes two arguments, a success callback and a failure callback. Each type is a function whose input argument is the value passed in to the constructor’s input argument, resolve or reject, when it is finally called
  4. Fn calls resolve(1) after an interval, at which point a successful callback is required

Take a tumble — 1

At this point in the analysis, it’s not hard to see,fnTo change the state, and then execute the callback function, is a typical publish-subscribe pattern. callthenMethod registers either a success or failure callback, which is executed depending on the result of the function execution passed to the constructor. Also, the specification mentions an instance of Promise whose THEN can be called multiple times, similar to being subscribed multiple times, so it’s easy to think of arrays to manage these callbacks. So with that in mind, we’re ready to start writing some code.

In combination with Promises/A+ specification, Promises only have three states: Pending, depressing and Rejected. We can get the following code. Because new calls are involved, we directly use class to deal with MyPromise

const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{
  
  status=PENDING
  fullfilledStack=[]
  rejectedStack=[]
  
  constructor(fn){
    const resolve=(value) = >{
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
    const reject=(reason) = >{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    this.fullfilledStack.push(res)
    this.rejectedStack.push(rej)
  }
}
Copy the code

This is a basic example. We collected and called callbacks in a similar way to eventEmitter, running 🌰 above. No problem, we’ve completed our scenario at 🌰.

Another feature of Promise is its ability to make chain calls, which means we can make repeated THEN calls. Currently, the code we’ve written supports only one THEN. How do we solve this problem?

First of all, to ensure that successive THEN can be continued, we use each then to return a new Promise instance to achieve this, then put method related code as follows:

then(res,rej){
  return new MyPromise((resolve,reject) = >{
    Execute resolve or reject, depending on the condition})}Copy the code

Since all instances of MyPromise are returned, the problem of successive calls to then methods is solved. Then we need to solve the case of multiple THEN, how to ensure that the value in each THEN is the return value of the previous THEN method. Reorganize what we’re going to do with the logic we learned in Aha — 1. In terms of the division of labor, FN is called by MyPromise’s constructor, which provides fn with two methods for calling all callbacks registered on THEN. Thus, fn needs to display to call resolve or reject. When we return the MyPromise instance in the THEN method, the FN here also needs to fetch the return value of the last successful callback and pass it to the next resolve

Take a tumble — 2

After the old Promise status is updated, the corresponding callback will be executed. We can take advantage of the closure feature and put the method to change the new Promise in the callback. And the result of the callback registered with the old Promise is simply executed, and the return value is passed to the new Promise’s resolve or Reject methods.

Combining these two, we can get code like 👇🏻

then(res:Fn,rej? :Fn){
  return new MyPromise((resolve,reject) = >{
    const resCb=(v) = >{
      resolve(res(v))
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r) = >{ reject(rej! (r)) }this.rejectedStack.push(rejCb)
  })
}
Copy the code

Change our 🌰 to a chained call, and try our new then method

const p=new MyPromise((res,rej) = >{
  setTimeout(() = >{
    res(1)},1000)
})
p
.then((v) = >{
  console.log(v)
  return 2
})
.then((v) = >{
  console.log(v)
  return 3
})
.then(v= >{
  console.log(v)
})

// outputs 1, 2, 3
Copy the code

The output was fine, but as I wrote it, I realized that there was a problem with the array of successful callbacks and the array of failed callbacks, and whether it was singleton or followed by each Promise instance. Here, the author analyzes the this point in the code execution. Although arrow functions are used inside then, then methods are called by each Promise object.

Take a tumble — 3

Each time we call THEN, a new Promise instance is generated, and the fn function that is executed to generate the new instance stuffs the callback array of the next Promise instance, so each Promise instance maintains its own array of success callbacks and failure callbacks, as we would expect.

Let’s take a look at the current code:


class MyPromise{

  fulfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value) = >{
      for(const cb of this.fulfilledStack){
        cb(value)
      }
    }
    const reject=(reason) = >{
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
    fn(resolve,reject)
  }

  then(onfulfilled,onrejected){
    return new MyPromise((resolve,reject) = >{
      const resCb=(v) = >{
        resolve(onfulfilled(v))
      }
      this.fulfilledStack.push(resCb)
      
      const rejCb=(r) = >{
        reject(onrejected(r))
      }
      this.rejectedStack.push(rejCb)
    })
  }
}

const p0=new MyPromise((resolve,reject) = >{
  setTimeout(() = >{
    resolve(1)},1000)})const p1 = p0.then((v) = >{ // cb1
  console.log(v)
  return 2
})
const p2 = p1.then((v) = >{ // cb2
  console.log(v)
  return 3
})
const p3 = p2.then((v) = >{ // cb3
  console.log(v)
})
Copy the code

Now that we’ve supported chained calls, let’s take a look at the implementation of the ContruStor and THEN methods in the MyPromise class. The constructor:

  1. Take fn as a function
  2. Declare two functions that execute the success and failure callbacks on the current MyPromise instance in turn
  3. Pass the declared two functions to fn as arguments for fn to call

Then:

  1. Accept onfulfilled and onRejected as parameters
  2. Return a new MyPromise instance
  3. Declare two functions that take care of value passing and retain the ability to change the new MyPromise state using closures. And pass the newly generated MyPromise instance with the return value of onfulfilled() or onRejected ()
  4. Push two functions, one into the array of the current MyPromise instance. The result is a callback to the old MyPromise instance that controls the state of the new MyPromise instance.

Let’s follow our 🌰 to see what happens when the code executes.

  1. On line 36 execution, object P0 is returned, and we register a timing function that will execute resolve(1) after 1s.
  2. On line 42, we call the then method on object P0 and pass in a function cb1. The implementation of the THEN method will return a new object P1, and we will push a method into the ledstack of object P0 that will execute the cb1 we passed in earlier and take its return value as an argument. Passed to the resolve method of the fn function when instantiated p1.
  3. Line 46, we’re calling the then method on P1, same as line 42, we’re going to return a P2 object, and we’re going to push a method on p1’s ledStack that will execute cb2, The return value of CB2 is passed to the resolve method in the FN function that instantiates the p2 object.
  4. The subsequent then calls will not be repeated. When the synchronized method is finished, 1s is elapsed, and resolve(1) of the FN method used to instantiate P0 will be executed, and the ledstack function of P0 will be executed with 1 as an argument. At this point, cb1 will formally execute, control to output 1, and pass the return value 2 of CB1 to RESOLVE of P1, and the function in LEDStack of P1 will execute sequentially with 2 as argument. And so on, eventually the console prints 1,2,3

So far, we have considered scenarios where the CB function returns a value directly, but we also need to consider scenarios where the CB function returns a new MyPromise instance, as shown in the demo below

const p=new MyPromise((resolve,reject) = >{
  setTimeout(() = >{
    resolve(1)},1000)
})
p
.then((v) = >{
  console.log(v)
  return new MyPromise((res) = >{
    setTimeout(() = > {
      res(2)},1000);
  })
})
.then((v) = >{
  console.log(v)
  return 3
})
.then(v= >{
  console.log(v)
})
Copy the code

How do we handle the situation where the cb function returns an instance of MyPromise and our then method eventually returns a new instance of MyPromise?

Take a tumble — 4

We’ve made then generate a new Promise instance each time and synchronize the state of the new instance with the old one. Now that CB returns a Promise instance, all we need to do is pass the resolve and reject methods of the Promise generated by then to the THEN of the Promise instance returned by CB.Also, we seem to have been missing the problem of method execution errors, which requires that the Promise be set torejectedState, let’s make up the link

then(res,rej){
  return new MyPromise((resolve,reject) = >{
    const resCb=(v) = >{
      try{
        const result = res(v)
        if(result instanceof MyPromise){
          // return from the res method returns an instance of MyPromise, such as return new MyPromise()
          // The state of the newly constructed MyPromise instance is controlled by the MyPromise instance returned by res
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(e)
      }
    }
    this.fullfilledStack.push(resCb)

    const rejCb=(r) = >{
      try {
        const result = rej(r)
        if(result instanceof MyPromise){
          result.then(resolve,reject)
        }else{
          resolve(result)
        }
      } catch (e){
        reject(r)
      }
    }
    this.rejectedStack.push(rejCb)
  })
}
Copy the code

Run the demo, the output is normal, the time interval is correct. At this point, the chain call problem has been solved. Back to Promises/A+, what we need to deal with is ** value transfer. ** Let’s look at the specification’s interpretation of value passing

 promise2 = promise1.then(onFulfilled, onRejected);
Copy the code
  1. If onFulfilled is not a function and promise1 is fulfilled, promise2 must be fulfilled with the same value as promise1.
  2. If onRejected is not a function and promise1 is rejected, promise2 must be rejected with the same reason as promise1.

We worked on both scenarios, and it’s not hard to get the following code

  then(res,rej){
    return new MyPromise((resolve,reject) = >{
      const resCb=(v) = >{
        if(typeofres ! = ='function') {return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // return from the res method returns an instance of MyPromise, such as return new MyPromise()
            // The state of the newly constructed MyPromise instance is controlled by the MyPromise instance returned by res
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      
      const rejCb=(r) = >{
        if(typeofrej ! = ='function') {return reject(r)
        }
        try {
          constresult = rej! (r)if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
    })
  }
Copy the code

So far, we’ve been talking about how promises handle asynchrony. The order of execution is always new Promise, bind the callback with then, change the Promise state, execute the corresponding callback, but if we don’t have setTimeout in our demo, the order is New Promise, change the Promise state, register the callback, Obviously, the callback will not be executed at this point, so we need to adjust our THEN methods to ensure that the Promise state has changed before the THEN execution and the callback will be executed correctly


  then(res,rej){
    return new MyPromise((resolve,reject) = >{
      // ...
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      // ...
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }
}


Copy the code

Value passing and compatible synchronization have been completed, and in this change, we have added a value field for MyPromise to pass the value of the current instance to the corresponding callback function.

The implementation of the then method is complete, and we will improve the member methods catch, resolve, Reject, all, race and other static methods. When you implement promise.all, you run into the same situation: how do you know if all promises have been fulfilled?

Take a tumble — 5

We can use a simple counter to determine if each Promise has been fulfilled


  catch(cb:Fn){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve= >{
      resolve(val)
    })
  }

  static reject(val){
    return new MyPromise((resolve,reject) = >{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject) = >{
      var count=0
      var result=[]
      arr.forEach((p,index) = >{
        MyPromise.resolve(p).then((val) = >{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason= >{
          reject(reason)
        })
      })
    })
  }
  
  static race(arr){
    return new MyPromise((resolve,reject) = >{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v= >resolve(v),
          r= >reject(r)
        )
      }
    })
  }
Copy the code

When we verified our logic with the demo, we found that multiple MyPromises still produced multiple results when executing race, which was not what we expected. We went back to see what the problem was.

Take a tumble — 6

We added the concept of states to promises, but there was no restriction on changes between states, resulting in a Promise state that could change multiple times and trigger multiple callbacks.

Now we add this constraint, and the code is as follows

constructor(fn){
  const resolve=(value:PValue) = >{
    if(this.status===PENDING){
      this.status=FULLFILLED
      this.value=value
      for(const cb of this.fullfilledStack){
        cb(value)
      }
    }
  }
  const reject=(reason) = >{
    if(this.status===PENDING){
      this.status=REJECTED
      this.value=reason
      for(const cb of this.rejectedStack){
        cb(reason)
      }
    }
  }
  fn(resolve,reject)
}
Copy the code

So far, we have completed a Promise of the core method, after a “aha”, also a complete understanding of handwritten a Promise to master the knowledge points

The complete code


const PENDING='pending'
const FULLFILLED='fullfilled'
const REJECTED='rejected'

class MyPromise{

  status=PENDING
  value=undefined
  fullfilledStack=[]
  rejectedStack=[]

  constructor(fn){
    const resolve=(value) = >{
      if(this.status===PENDING){
        this.status=FULLFILLED
        this.value=value
        for(const cb of this.fullfilledStack){
          cb(value)
        }
      }
    }
    const reject=(reason) = >{
      if(this.status===PENDING){
        this.status=REJECTED
        this.value=reason
        for(const cb of this.rejectedStack){
          cb(reason)
        }
      }
    }
    fn(resolve,reject)
  }

  then(res,rej){
    return new MyPromise((resolve,reject) = >{
      const resCb=(v) = >{
        if(typeofres ! = ='function') {return resolve(v)
        }
        try{
          const result = res(v)
          if(result instanceof MyPromise){
            // return from the res method returns an instance of MyPromise, such as return new MyPromise()
            // The state of the newly constructed MyPromise instance is controlled by the MyPromise instance returned by res
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(e)
        }
      }
      this.fullfilledStack.push(resCb)
      this.status===FULLFILLED && resCb(this.value)
      
      const rejCb=(r) = >{
        if(typeofrej ! = ='function') {return reject(r)
        }
        try {
          const result = rej(r)
          if(result instanceof MyPromise){
            result.then(resolve,reject)
          }else{
            resolve(result)
          }
        } catch (e){
          reject(r)
        }
      }
      this.rejectedStack.push(rejCb)
      this.status===REJECTED && rejCb(this.value)
    })
  }

  catch(cb){
    this.then(undefined,cb)
  }

  static resolve(val){
    if(val instanceof MyPromise) return val
    return new MyPromise(resolve= >{
      resolve(val)
    })
  }

  static reject(vale){
    return new MyPromise((resolve,reject) = >{
      reject(val)
    })
  }

  static all(arr){
    return new MyPromise((resolve,reject) = >{
      var count=0
      var result=[]
      arr.forEach((p,index) = >{
        MyPromise.resolve(p).then((val) = >{
          count++
          result[index]=val
          if(count === arr.length){
            resolve(result)
          }
        },reason= >{
          reject(reason)
        })
      })
    })
  }

  static race(arr){
    return new MyPromise((resolve,reject) = >{
      for(const p of arr){
        MyPromise.resolve(p)
        .then(
          v= >resolve(v),
          r= >reject(r)
        )
      }
    })
  }
}
Copy the code

Refer to the article

Interviewer: “Can you make a Promise by hand?”

9 k word | Promise/async/Generator principle parsing