Hand in hand teaching you to realize promise

Time:2021-1-16

preface

Many beginners of JavaScript have experienced the fear of being dominated by callback hell, until they mastered promise syntax. Although promise has been built into many languages for a long time, it is jQuery 1.5 that really promotes it in JavaScript$.ajaxThe refactoring supports promise, and its usage coincides with the chain call advocated by jQuery. After the birth of ES6, everyone began to enter the era of promise. Then es8 introduced async syntax to make the asynchronous writing of JavaScript more elegant.

Today, we’ll implement a promise step by step. If you haven’t used promise, it’s recommended that you be familiar with the promise syntax before reading this article.

Constructors

In the existingPromise/A+standardIt doesn’t specify where the promise object comes from in jQuery$.Deferred()Get the promise object. In ES6, get the promise object by instantiating the promise class. Here, we use es syntax to construct a class and return the promise object by instantiation. Because promise already exists, we temporarily name this class asDeferred。

class Deferred {
  constructor(callback) {
    const resolve = () => {
      // TODO
    }
    const reject = () => {
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      reject(error)
    }
  }
}

The constructor accepts a callback. When calling a callback, it needs to pass in two methods: resolve and reject.

Status of promise

Promise is divided into three states

Hand in hand teaching you to realize promise

  • ⏳pending: waiting, this is the initial state of promise;Hand in hand teaching you to realize promise
  • πŸ™†β€β™‚οΈfulfilled: finished. Call resolve normally;Hand in hand teaching you to realize promise
  • πŸ™…β€β™‚οΈrejected: rejected, internal error, or state after calling reject;Hand in hand teaching you to realize promise

We can see that promise has a state stored in the[[PromiseState]]In the middle. Let’s add a state for deferred.

//Definition of basic variable
const STATUS = {
  PENDING: 'PENDING',
  FULFILLED: 'FULFILLED',
  REJECTED: 'REJECTED'
}

class Deferred {
  constructor(callback) {
    this.status = STATUS.PENDING

    const resolve = () => {
      // TODO
    }
    const reject = () => {
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

Another interesting thing here is that in the early implementation of browsers, the fully state was resolved, which was obviously inconsistent with promise specification. Now, of course, it’s fixed.

Hand in hand teaching you to realize promise

Internal results

Apart from the status, promise has an internal result[[PromiseResult]], which is used to hold the value accepted by resolve / reject.

Hand in hand teaching you to realize promise

Hand in hand teaching you to realize promise

Continue to add an internal result to the constructor.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    const resolve = value => {
      this.value = value
      // TODO
    }
    const reject = reason => {
      this.value = reason
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

Save callback

When using promise, we usually call the promise object.thenMethod, in the promise state tofulfilledorrejectedAt the same time, get the internal results, and then do the follow-up processing. So in the constructor, we need to construct two arrays to store the.thenMethod.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    const resolve = value => {
      this.value = value
      // TODO
    }
    const reject = reason => {
      this.value = reason
      // TODO
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

resolveAndreject

modify state

Next, we need to implement the resolve and reject methods, which will change the state of the project object when they are called. After any method is called, other methods cannot be called.

new Promise((resolve, reject) => {
    setTimeout(() => {
    resolve('πŸ™†β€β™‚οΈ')
  }, 500)
  setTimeout(() => {
    reject('πŸ™…β€β™‚οΈ')
  }, 800)
}).then(
  () => {
    console.log('fulfilled')
  },
  () => {
    console.log('rejected')
  }
)

Hand in hand teaching you to realize promise

At this point, the console will only print out thefulfilledAnd it’s not going to happenrejected。

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    Let called // used to determine whether the state has been modified
    const resolve = value => {
            if (called) return
      called = true
      this.value = value
      //Modification status
      this.status = STATUS.FULFILLED
    }
    const reject = reason => {
            if (called) return
      called = true
      this.value = reason
      //Modification status
      this.status = STATUS.REJECTED
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

Call callback

After modifying the status, the promise that gets the result will call the callback passed in by the then method.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    Let called // used to determine whether the state has been modified
    const resolve = value => {
            if (called) return
      called = true
      this.value = value
      //Modification status
      this.status = STATUS.FULFILLED
      //Call callback
      for (const fn of this.resolveQueue) {
        fn(this.value)
      }
    }
    const reject = reason => {
            if (called) return
      called = true
      this.value = reason
      //Modification status
      this.status = STATUS.REJECTED
      //Call callback
      for (const fn of this.rejectQueue) {
        fn(this.value)
      }
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

Students familiar with JavaScript event system should know that,promise.thenMethod is placed in the micro task queue and called asynchronously.

Hand in hand teaching you to realize promise

Therefore, we need to put the callback call into the asynchronous queue. Here, we can put it into the setTimeout to delay the call. Although it doesn’t conform to the specification, it’s not easy.

class Deferred {
  constructor(callback) {
    this.value = undefined
    this.status = STATUS.PENDING

    this.rejectQueue = []
    this.resolveQueue = []

    Let called // used to determine whether the state has been modified
    const resolve = value => {
            if (called) return
      called = true
      //Asynchronous call
      setTimeout(() => {
          this.value = value
        //Modification status
        this.status = STATUS.FULFILLED
        //Call callback
        for (const fn of this.resolveQueue) {
          fn(this.value)
        }
      })
    }
    const reject = reason => {
            if (called) return
      called = true
      //Asynchronous call
      setTimeout(() =>{
        this.value = reason
        //Modification status
        this.status = STATUS.REJECTED
        //Call callback
        for (const fn of this.rejectQueue) {
          fn(this.value)
        }
      })
    }
    try {
      callback(resolve, reject)
    } catch (error) {
      //In case of exception, reject it directly
      reject(error)
    }
  }
}

Then method

Next, we need to implement the then method. Those who have used promise must know that the then method can continue to be chained, so then must return a promise object. But inPromise/A+In the specification, it is clearly stipulated that the then method returns a new promise object instead of directly returning this. We can verify this by the following code.

Hand in hand teaching you to realize promise

You can see thatp1Objects andp2Is two different objects, and the then method returnsp2Object is also an instance of promise.

In addition, the then method also needs to determine the current state, if the current state is notpendingState, you can call the incoming callback directly without putting it into the queue to wait.

class Deferred {
  then(onResolve, onReject) {
    if (this.status === STATUS.PENDING) {
      //Put the callback in the queue
      const rejectQueue = this.rejectQueue
      const resolveQueue = this.resolveQueue
      return new Deferred((resolve, reject) => {
        //Staging to successful callback pending call
        resolveQueue.push(function (innerValue) {
          try {
            const value = onResolve(innerValue)
            //Change the current promise state
            resolve(value)
          } catch (error) {
            reject(error)
          }
        })
        //Staging to failed callbacks waiting to be called
        rejectQueue.push(function (innerValue) {
          try {
            const value = onReject(innerValue)
            //Change the current promise state
            resolve(value)
          } catch (error) {
            reject(error)
          }
        })
      })
    } else {
      const innerValue = this.value
      const isFulfilled = this.status === STATUS.FULFILLED
      return new Deferred((resolve, reject) => {
        try {
          const value = isFulfilled
            ? onresolve (innervalue) // call onresolve successfully
            : onreject (innervalue) // call onreject in failure state
          Resolve (value) // returns the result to then
        } catch (error) {
          reject(error)
        }
      })
    }
  }
}

Now that our logic can basically run through, let’s try to run a piece of code:

new Deferred(resolve => {
  setTimeout(() => {
    resolve(1)
  }, 3000)
}).then(val1 => {
  console.log('val1', val1)
  return val1 * 2
}).then(val2 => {
  console.log('val2', val2)
  return val2
})

After 3 seconds, the console displays the following results:

Hand in hand teaching you to realize promise

It can be seen that this is basically in line with our expectations.

Value penetration

If we call then without passing in any parameters, according to the specification, the current promise value can be passed through to the next then method. For example, the following code:

new Deferred(resolve => {
  resolve(1)
})
  .then()
  .then()
  .then(val => {
    console.log(val)
  })

Hand in hand teaching you to realize promise

You don’t see any output in the console, but you can see the correct result when you switch to promise.

Hand in hand teaching you to realize promise

To solve this problem, we only need to determine whether the parameter is a function when then is called. If not, we need to give a default value.

const isFunction = fn => typeof fn === 'function'

class Deferred {
  then(onResolve, onReject) {
    //Solution value penetration
    onReject = isFunction(onReject) ? onReject : reason => { throw reason }
    onResolve = isFunction(onResolve) ? onResolve : value => { return value }
    if (this.status === STATUS.PENDING) {
      // ...
    } else {
      // ...
    }
  }
}

Hand in hand teaching you to realize promise

Now we can get the right results.

One step away

Now we are only one step away from the perfect implementation of the then method, which is what we pass in by calling the then methodonResolve/onRejectCallback, you also need to determine their return value. If the callback returns a promise object, what should we do? Or if there is a circular reference, what should we do?

We’re getting it in frontonResolve/onRejectAfter the return value of theresolveperhapsresolveNow we need to process their return values.

then(onResolve, onReject) {
  //The solution value penetration code has been omitted
  if (this.status === STATUS.PENDING) {
    //Put the callback in the queue
    const rejectQueue = this.rejectQueue
    const resolveQueue = this.resolveQueue
    const promise = new Deferred((resolve, reject) => {
      //Staging to successful callback pending call
      resolveQueue.push(function (innerValue) {
        try {
          const value = onResolve(innerValue)
-         resolve(value)
+         doThenFunc(promise, value, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
      //Staging to failed callbacks waiting to be called
      rejectQueue.push(function (innerValue) {
        try {
          const value = onReject(innerValue)
-         resolve(value)
+         doThenFunc(promise, value, resolve, reject)
        } catch (error) {
          reject(error)
        }
      })
    })
    return promise
  } else {
    const innerValue = this.value
    const isFulfilled = this.status === STATUS.FULFILLED
    const promise = new Deferred((resolve, reject) => {
      try {
        const value = isFulfilled
        ? onresolve (innervalue) // call onresolve successfully
        : onreject (innervalue) // call onreject in failure state
-       resolve(value)
+       doThenFunc(promise, value, resolve, reject)
      } catch (error) {
        reject(error)
      }
    })
    return promise
  }
}

Return value judgment

When we use promise, we often return a new promise in the then method, and then pass the internal result of the new promise to the later then method.

fetch('server/login')
    .then(user => {
      //Returns a new promise object
      return fetch(`server/order/${user.id}`)
    })
    .then(order => {
      console.log(order)
    })
function doThenFunc(promise, value, resolve, reject) {
  //If value is a promise object
  if (value instanceof Deferred) {
    //Call the then method and wait for the result
    value.then(
      function (val) {
          doThenFunc(promise, value, resolve, reject)
        },
      function (reason) {
        reject(reason)
      }
    )
    return
  }
  //If it is not a promise object, it returns directly
  resolve(value)
}

Judge circular reference

If the return value of the callback function of the current then method is a new promise object generated by the current then method, it is considered to be a circular reference

Hand in hand teaching you to realize promise

The new promise object returned by the then methodp1, as the return value in the callback, an exception will be thrown. Because according to the previous logic, the code will always be trapped in this logic.

Hand in hand teaching you to realize promise

Therefore, we need to prevent in advance and throw out errors in time.

function doThenFunc(promise, value, resolve, reject) {
  //Circular reference
  if (promise === value) {
    reject(
        new TypeError('Chaining cycle detected for promise')
    )
    return
  }
  //If value is a promise object
  if (value instanceof Deferred) {
    //Call the then method and wait for the result
    value.then(
      function (val) {
          doThenFunc(promise, value, resolve, reject)
        },
      function (reason) {
        reject(reason)
      }
    )
    return
  }
  //If it is not a promise object, it returns directly
  resolve(value)
}

Now let’s try to return a new promise object in then.

const delayDouble = (num, time) => new Deferred((resolve) => {
  console.log(new Date())
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

new Deferred(resolve => {
  setTimeout(() => {
    resolve(1)
  }, 2000)
})
  .then(val => {
    console.log(new Date(), val)
    return delayDouble(val, 2000)
  })
  .then(val => {
    console.log(new Date(), val)
  })

Hand in hand teaching you to realize promise

The above results are also perfectly in line with our expectations.

Catch method

In fact, the catch method is very simple, which is equivalent to the abbreviation of then method.

class Deferred {
  constructor(callback) {}
  then(onResolve, onReject) {}
  catch(onReject) {
    return this.then(null, onReject)
  }
}

Static method

resolve/reject

The promise class also provides two static methods to directly return the promise object whose state has been fixed.

class Deferred {
  constructor(callback) {}
  then(onResolve, onReject) {}
  catch(onReject) {}
  
  static resolve(value) {
    return new Deferred((resolve, reject) => {
      resolve(value)
    })
  }

  static reject(reason) {
    return new Deferred((resolve, reject) => {
      reject(reason)
    })
  }
}

all

The all method accepts an array of promise objects and changes the state of all promise objects in the array tofulfilledThe result is also an array, and each value of the array corresponds to the internal result of the promise object.

First of all, we need to determine whether the incoming parameter is an array, and then construct a result array and a new promise object.

class Deferred {
  static all(promises) {
    //Non array parameter, throw exception
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

        //Used to store the results of each promise object
    const result = []
    const length = promises.length
    //If remaining is set to zero, all promise objects have been fulfilled
    let remaining = length 
    const promise = new Deferred(function (resolve, reject) {
      // TODO
    })
        return promise
  }
}

Next, we need to make a judgment to intercept the resolve of each promise object. Each resolve needs toremainingMinus one untilremainingZero.

class Deferred {
  static all(promises) {
    //Non array parameter, throw exception
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

    Const result = [] // used to store the result of each promise object
    const length = promises.length

    let remaining = length
    const promise = new Deferred(function (resolve, reject) {
      //If the array is empty, an empty result is returned
      if (promises.length === 0) return resolve(result)

      function done(index, value) {
        doThenFunc(
          promise,
          value,
          (val) => {
            //The result of resolve is put into result
            result[index] = val
            if (--remaining === 0) {
              //If all the promises have returned results
              //Then run the following logic
              resolve(result)
            }
          },
          reject
        )
      }
      //Put in asynchronous queue
      setTimeout(() => {
        for (let i = 0; i < length; i++) {
          done(i, promises[i])
        }
      })
    })
        return promise
  }
}

Now we use the following code to judge whether the logic is correct. As expected, after the code runs, after 3 seconds, the console will print an array[2, 4, 6]。

const delayDouble = (num, time) => new Deferred((resolve) => {
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

console.log(new Date())
Deferred.all([
  delayDouble(1, 1000),
  delayDouble(2, 2000),
  delayDouble(3, 3000)
]).then((results) => {
  console.log(new Date(), results)
})

Hand in hand teaching you to realize promise

The above running results are basically in line with our expectations.

race

The race method also accepts an array of promise objects, but it only needs one promise to be changed tofulfilledThe status will return the result.

class Deferred {
  static race(promises) {
    if (!Array.isArray(promises)) {
      return Deferred.reject(new TypeError('args must be an array'))
    }

    const length = promises.length
    const promise = new Deferred(function (resolve, reject) {
      if (promises.length === 0) return resolve([])

      function done(value) {
        doThenFunc(promise, value, resolve, reject)
      }

      //Put in asynchronous queue
      setTimeout(() => {
        for (let i = 0; i < length; i++) {
          done(promises[i])
        }
      })
    })
    return promise
  }
}

Next, let’s change the previous case to race. As expected, after the code runs, after one second, the console will print a 2.

const delayDouble = (num, time) => new Deferred((resolve) => {
  setTimeout(() => {
    resolve(2 * num)
  }, time)
})

console.log(new Date())
Deferred.race([
  delayDouble(1, 1000),
  delayDouble(2, 2000),
  delayDouble(3, 3000)
]).then((results) => {
  console.log(new Date(), results)
})

Hand in hand teaching you to realize promise

The above running results are basically in line with our expectations.

summary

A simple version of the promise class has been implemented. Some details are omitted here. The complete code can be accessedgithub. The emergence of promise has laid a solid foundation for the later async syntax. In the next blog, you can have a good chat about the asynchronous programming history of JavaScript, and accidentally dig a hole for yourself…

Hand in hand teaching you to realize promise