Promise in ECMAScript6 was a game-changing API that solved the asynchronous problem that had plagued front-end developers, giving us a better weapon against asynchronous callbacks

The code address

preface

Faced with the promise of seeing each other every day, I wonder if you have some of the following questions

  1. New Promise((resolve,reject) =>{}), resolve,reject
  2. Why is then or catch executed after resolve?
  3. Why is it possible to chaining.then, and still do it synchronously?
  4. Why is promise.resolve() invoked when the following function supports promises?
  5. How is promise.all implemented?
  6. Were you confused by the promise question in the exam?

Let’s understand the realization principle of Promise, all the answers to the questions surfaced naturally ~

Promise has subtly helped us simplify complex asynchronous code and reduce logical difficulty. It is not an exaggeration to say that Promise is an epoch-making asynchronous solution. It has well demonstrated the open and closed principle and solved the problem of high coupling

A quick tidbit, an asynchronous scheme like PrMISE already existed before ES6 was released, and jquery.deferred (), a similar technical scheme, has been applied in jquery ajax, for those who are interested, you can check it out

$.ajax("test.html")
.done(function(){ alert("Ha ha, it worked!"); })
.fail(function(){ alert("Wrong!); });
Copy the code

Simplified Primise

The implementation of the base version is simple, but it explains a lot

You are advised to copy the code to the local PC and view the code execution process through a breakpoint

const PEDDING = 'pending' // Wait state
const FULFILLED = 'fulfilled' // Success status
const REJECTED = 'rejected' // Failed state

class APromise {
  constructor(executor) {
    this.status = PEDDING // Initialization state
    this.value = undefined // Success data
    this.reason = undefined // Cause of failure
    this.onFulfilledCallbacks = [] // Save the successful callback queue
    this.onRejectCallbacks = [] // Save the failed callback queue

    const resolve = (data) = > {
      if (this.status == PEDDING) {
        this.status = FULFILLED
        this.value = data
      }
      this.onFulfilledCallbacks.map((e) = > e())
    }

    const reject = (err) = > {
      if (this.status == PEDDING) {
        this.status = REJECTED
        this.reason = err
      }
      this.onRejectCallbacks.map((e) = > e())
    }
    try {
      executor(resolve, reject)
    } catch (error) {
      rejected(error)
    }
  }

  then(onFulfilled, onRejected) {
    if (this.status == FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status == REJECTED) {
      onRejected(this.reason)
    }
    if (this.status == PEDDING) {
      this.onFulfilledCallbacks.push(() = > {
        onFulfilled(this.value)
      })
      this.onRejectCallbacks.push(() = > {
        onRejected(this.reason)
      })
    }
  }
}

new APromise((resolve, reject) = > {
  console.log('Start a callback')
  setTimeout(() = > {
    console.log('Execute callback')
    resolve(11111)},1000)
}).then(
  (value) = > {
    console.log('Successful callback', value)
  },
  (err) = > {
    console.log('Failed callback', err)
  }
)
Copy the code

Code flow

  1. Initialize APromise and begin executing constructor in class
  2. Initialize some state values for the current PROMISE and the resolve, reject functions in constructor
  3. Finally, the resolve and reject functions are given as arguments to the Promise callback, and the function is executed at the same time, printing the start callback
  4. Run setTimeout and start parsing the then function
  5. If the status is successful or failed, the callback is directly executed; if the status is PEDding, the success and failure callback functions are stored
  6. After 1s setTimeout completes, resolve executes resolve from constructor
  7. The resolve function stores the success, 11111, or failure callback when initialized before execution. Then

Logical flow chart

The basic version of the implementation, no chain-call, no THEN penetration, no catch, only the most basic logic

Here we answer the questions raised in the introduction

  1. New Promise((resolve,reject) =>{}), resolve,reject

    A: New executes the constructor from the Promise, declaring resolve and reject, and passing arguments to the Promise callback when it executes

  2. Why is then or catch executed after resolve?

    A: Because in the initial phase, we store the success and failure callbacks of the current Promise in pedding state. When we execute resolve, the state of the current Promise changes and the stored callback function starts to be executed. If it is not the padding, the callback function is immediately executed

The latter question we can’t explain yet, but as we further implement, the answer will emerge

Official (chain callback, then value penetration,.catch, etc.)

Chain callback

We usually write promises with multiple dot then, and in multiple dot then we change asynchronous code into synchronous code blocks, but in our basic version of promise we can’t show chained calls, because the function doesn’t return any value after we execute dot then, so the then method, in that sense, We rewrote the promise’s.then parsing process to try to support chain calls

  1. Each.then needs to return a promise to trigger the next.then
  2. Various cases of then callback functions need to be judged, for example. Does then return a string or a promise? If so, add a chained callback to trigger the parent’s resolve
  3. Then function execution needs to be wrapped with setTimeout to add macro tasks
/** * Then returns either a normal value or a promise, as defined by the PromiseA+ standard. /** * Then returns a promise, as defined by the PromiseA+ standard@param {*} Promise current promise *@param {*} X Current return value *@param {*} Resolve Successful callback *@param {*} Reject Failed callback */
const resolvePromise = (promise, x, resolve, reject) = > {
  if (promise === x) {
    return reject(new TypeError('Loop call detected for promise')) // 'Chaining cycle detected for promise #<Promise>'
  }
  let called = false
  if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {
    try {
      const then = x.then
      if (typeof then === 'function') {
        then.call(
          x,
          (y) = > {
            if (called) return
            called = true
            resolvePromise(promise, y, resolve, reject)
          },
          (r) = > {
            if (called) return
            called = true
            reject(r)
          }
        )
      } else {
        resolve(x)
      }
    } catch (err) {
      if (called) return
      called = true
      reject(err)
    }
  } else {
    resolve(x)
  }
}

then(onFulfilled, onRejected) {
  let apromise = new APromise((resolve, reject) = > {
    if (this.status === FULFILLED) {
      setTimeout(() = > {
        try {
          const x = onFulfilled(this.value)
          resolvePromise(apromise, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)}if (this.status === REJECTED) {
      setTimeout(() = > {
        try {
          const x = onRejected(this.reason)
          resolvePromise(apromise, x, resolve, reject)
        } catch (err) {
          reject(err)
        }
      }, 0)}if (this.status === PEDDING) {
      this.onFulfilledCallbacks.push(() = > {
        setTimeout(() = > {
          try {
            const x = onFulfilled(this.value)
            resolvePromise(apromise, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)})this.onRejectCallbacks.push(() = > {
        setTimeout(() = > {
          try {
            const x = onRejected(this.reason)
            resolvePromise(apromise, x, resolve, reject)
          } catch (err) {
            reject(err)
          }
        }, 0)})}})return apromise
}
Copy the code

After the internal promise processing above, the running logic of the function has changed a lot

The intuitive logic is that

The actual running logic is to create a Promise again in each.then, so that it can be called the next time, and to process the.then callback to distinguish between.then which returns a Promise object and an ordinary object, so that the.then chain calls are implemented

When there is a return promise in then, the logic will change, which is mainly reflected in the resolvePromise function

Then the value through

Let’s start with a scenario

new APromise((resolve, reject) = > {
  resolve(11111);
})
  .then()
  .then()
  .then(data= > {
    console.log('Successful callback', data);
  }, err= > {
    console.log('Failed callback', err);
  })
Copy the code

There is no way to pass resolve to the bottom. Then, so we need to do something about this case

then(onFulfilled, onRejected) {
  If the THEN is empty, the previous resolve value is manually inserted into the next THEN
  onFulfilled = typeof onFulfilled == 'function' ? onFulfilled : (data) = > data
  onRejected = typeof onRejected == 'function' ? onRejected : (err) = > { throw err }
  let apromise = new APromise((resolve, reject) = > {
    / /...
  })
  return apromise
}
Copy the code

When we process the callback function in the then value, the function actually run becomes

new APromise((resolve, reject) = > {
  resolve(11111);
})
  .then((data) = > data)
  .then((data) = > data)
  .then(data= > {
    console.log('Successful callback', data);
  }, err= > {
    console.log('Failed callback', err);
  })
Copy the code

This implements the THEN penetration problem

.catch

Currently, the second argument to the.then error callback does not support the.catch method, so we can add the catch method to the prototype chain

Catch is also a encapsulation of the.then method, except that there is no success callback, only failure callback

APromise.prototype.catch = function (errCallback) {
  return this.then(null, errCallback)
}
Copy the code

.finally

Since finally cannot predict the final state of a promise, its callback takes no arguments. It is only used when the promise is executed regardless of the final result

One thing to note is that if there is a Promise in finally, you need to wait for the Promise to complete

APromise.prototype.finally = function (callBack) {
  return this.then(
    (data) = > {
      return APromise.resolve(callBack()).then(() = > data)
    },
    (err) = > {
      return APromise.reject(callBack()).then(() = > {
        throw err
      })
    }
  )
}
Copy the code

A little something about Finally

Promise.resolve(2).then(() = > {}, () = > {}) // undefined is passed

Promise.resolve(2).finally(() = > {}, () = > {}) // Finally itself receives no arguments, but puts the last callback data into the next callback
Copy the code

Promise.resolve()

Calling promise.resolve () returns a true Promise and directly returns the success callback

APromise.resolve = function (data) {
  return new APromise((resolve, reject) = > {
    resolve(data)
  })
}
Copy the code

Promise.reject()

Calling promise.resolve () returns a true Promise and directly returns the failure callback

APromise.reject = function (data) {
  return new APromise((resolve, reject) = > {
    reject(data)
  })
}
Copy the code

Promise.race()

When the race method is called, it must pass in an array of different types and function types. During initialization, a promise is created again, and race’s resolve is triggered when one of the promise objects in the array is first executed. After the subsequent execution, the state of the race has changed and cannot be executed again

/** * Execute multiple promises at the same time, but return the result returned first *@param {*} promiseList
 * @returns* /
APromise.race = function (promiseList) {
  if (!Array.isArray(promiseList)) {
    throw new TypeError('Must pass array')}return new APromise((resolve, reject) = > {
    promiseList.forEach((item) = > {
      if (item && typeof item.then == 'function') {
        item.then(resolve, reject)
      } else {
        resolve(item)
      }
    })
  })
}


let p1 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('ok1')},3000)})let p2 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    reject('ok2')},2000)
})

APromise.race([1, p1, p2]).then(
  (data) = > {
    console.log('success1', data)
  },
  (err) = > {
    console.log('error1', err)
  }
)
Copy the code

Promise.all()

The implementation logic of all is very simple. For all, a promise is created, which internally records the success of the current incoming list state. When all then data is successful, resolve is called, and when one fails, reject is called

/** * Execute multiple promises at the same time, wait for the result of each promise, and return it together@param {} promiseList 
 * @returns * /
APromise.all = function (promiseList) {
  if (!Array.isArray(promiseList)) {
    throw new TypeError('Must be an array')}return new APromise((resolve, reject) = > {
    const resulteArr = []
    const len = promiseList.length
    let currentIndex = 0
    const getResult = (key, val) = > {
      resulteArr[key] = val
      if (++currentIndex == len) {
        resolve(resulteArr)
      }
    }
    for (let i = 0; i < len; i++) {
      const val = promiseList[i]
      if (val && typeof val.then === 'function') {
        val.then((data) = > {
          getResult(i, data)
        }, reject)
      } else {
        getResult(i, val)
      }
    }
  })
}

let p1 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('ok1');
  }, 1000);
})

let p2 = new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('ok2');
  }, 2000);
})

Promise.all([1.2.3,p1,p2]).then(data= > {
  console.log('success', data);
}, err= > {
  console.log('error', err);
})
Copy the code

Promise.any()

The implementation is very similar to all, which is the complete opposite of all

/** * any is the exact opposite of all@param {*} promiseList
 * @returns* /
APromise.any = function (promiseList) {
  if (!Array.isArray(promiseList)) {
    throw new TypeError('Must be an array')}return new APromise((resolve, reject) = > {
    const resultArr = []
    const len = promiseList.length
    let currentIndex = 0
    const getResult = (index, err) = > {
      resultArr[index] = err
      if (++currentIndex == len) {
        reject(resultArr)
      }
    }
    promiseList.map((res, index) = > {
      if (res && typeof res.then == 'function') {
        res.then(resolve, (err) = > {
          getResult(index, err)
        })
      } else {
        resolve(res)
      }
    })
  })
}

let p3 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    reject('err3')},1000)})let p4 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    reject('err4')},2000)
})

APromise.any([p3, p4]).then(
  (data) = > {
    console.log('success', data)
  },
  (err) = > {
    console.log('error', err)
  }
)
Copy the code

Promise.allSettled()

AllSettled is the tool method added to ES2020, in a word: it is the promise that will never fail. All

/** * Save all successes and failures *@param {*} promiseList 
 * @returns * /
APromise.allSettled = function (promiseList) {
  if (!Array.isArray(promiseList)) {
    throw new TypeError('Must be an array')}return new APromise((resolve, reject) = > {
    const resultArr = []
    const len = promiseList.length
    let currentIndex = 0
    const getResult = (index, data, status) = > {
      if (status == FULFILLED) {
        resultArr.push({
          status: status,
          value: data,
        })
      }
      if (status == REJECTED) {
        resultArr.push({
          status: status,
          reason: data,
        })
      }
      if (++currentIndex == len) {
        resolve(resultArr)
      }
    }
    promiseList.map((res, index) = > {
      if (res && typeof res.then == 'function') {
        res.then(
          (data) = > {
            getResult(index, data, FULFILLED)
          },
          (err) = > {
            getResult(index, err, REJECTED)
          }
        )
      } else {
        getResult(index, res, FULFILLED)
      }
    })
  })
}

let p1 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('ok1')},3000)})let p2 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('ok2')},2000)})let p3 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    reject('err3')},1000)})let p4 = new APromise((resolve, reject) = > {
  setTimeout(() = > {
    reject('err4')},2000)
})

APromise.allSettled([1.2.3, p1, p2, p3, p4]).then((res) = > {
  console.log('success', res)
})
Copy the code

Test functions

-g Promises -aplus-tests install -g Promises -aplus-tests

Promises -aplus-tests xxxx.js

Add the following code at the end of the test file

PromiseA + if there are no errors

APromise.defer = APromise.deferred = function () {
  let dfd = {}
  dfd.promise = new APromise((resolve, reject) = > {
    dfd.resolve = resolve
    dfd.reject = reject
  })
  return dfd
}

module.exports = APromise
Copy the code

The source address

github-promise

You can use chrome DevTool or Vscode Debug to view the code running process with breakpoints to understand the promise running logic

Refer to the link

Re-learn the Promise, based on A+ specification to implement it, thanks to the nuggets @ Guan ER Promise interpretation article, greatly reducing the threshold of in-depth Promise

PromiseA + specification

MDN-Promise

45 Promise interview questions