preface

Hello everyone! I am one!

I’m sure you’ve all worked with Promises in both front-end and back-end development. In this article, we’ll walk you through “step-by-step” Promises that meet Promises/A+ specifications, and explore some of the ways Promises are made and how third-party extensions can be implemented.

By reading this article you can learn:

  • Handwriting implements promises that conform to the specification
  • usepromises-aplus-testsConduct specification testing
  • masterPromise.all.Promise.race.Promise.resolve.Promise.rejectAnd so on realization principle
  • Get to know some of the extensions to Promise in Node

In order to better understand and master promises, let’s introduce some Promise basics before we get down to business.

What is asynchrony

1.1 Why asynchrony exists in JS

As we all know, JS is a single-threaded language. The so-called single-threaded language means that you can only do one thing at a time, and other things can only queue up behind you.

In a browser, there are a lot of requests during page loading, and when a network request is not answered, the page waits around, unable to do anything else.

Therefore, ASYNCHRONOUS design in JS, that is, after sending the network request can continue to process other operations, and the data returned by the network request can be received and processed through the callback function, so as to ensure the normal operation of the page.

1.2 Asynchronous Solution

Take a look at the Node code below

var fs = require('fs')
fs.readFile('data.json', (err, data) => {
    console.log(data.toString())
})
Copy the code

The second argument to the fs.readFile method is a function that does not execute immediately but waits until the result of the readFile is available. This is the function called callback

1.3 Callback to Hell

When dealing with multiple asynchronous requests, nested one after the other, it’s easy to create callback hell. Look at the Node code below

const fs = require('fs')
fs.readFile('data1.json', (err, data1) => {
    fs.readFile('data2.json', (err, data2) => {
        fs.readFile('data3.json', (err, data3) => {
            fs.readFile('data4.json', (err, data4) => {
 console.log(data4.toString())  })  })  }) }) Copy the code

Rewrite with Promise

const fs = require('fs')
const readFilePromise = (file) = > {
 return new Promise((resolve, reject) = > {
   fs.readFile(file, (err, data) => {
     if (err) {
 reject(err)  }  resolve(data)  })  }) } readFilePromise('data1.json') .then(data1= > {  return readFilePromise('data2.json') }).then(data2= > {  return readFilePromise('data3.json') }).then(data3= > {  return readFilePromise('data4.json') }).then(data4= > {  console.log(data4.toString()) }).catch(err= > {  console.log(err) }) Copy the code

Question to consider: Did Promise really replace callback?

Promises are just a change in the readability of asynchronous manipulation code. They do not change the nature of asynchronous execution in JS, nor do they replace callback in JS. Meanwhile, callback is also used in Promise. The arguments to then() of the instance are successful and failed functions, namely callback functions.

Two, the implementation of Promise

The project address for this article: github.com/Yangjia23.. …

2.1 Basic Implementation

2.1.1 Executor Executor

First, Promise is a class, and you need to use new to create an instance

  • new Promise((resolve, reject) => {})The argument passed in is a function calledexecutorExecutor, which executes immediately by default
  • executorTwo arguments are passed in for executionresolve, reject, respectively, successful function and failed function
  • resolve, rejectThe two executing functions are not static properties on the Promise class, nor are they methods on the instance, but are a normal function
class Promise {
 constructor (executor) {
    / / success
    const resolve = (a)= > {}
    / / fail
 const reject = (a)= > {}  // Execute immediately  executor(resolve, reject)  } } Copy the code

2.1.2 Three states

About Promise states

  • A promise has three states: pending, which is a pity and rejected. The default state is pending

  • The promise state can only be changed from pending to fulfilled or Rejected

To learn more about Promises, see Promises/A+ Specification: Promise-States

ReadFilePromise, for example

  • Is called when reading the file successfullyresolveFunction, passing in what was read, indicating successful execution, at which point the state should befulfilledSuccessful state
  • Failed to read the filerejectFunction, passing in the cause of the failure, indicating that execution failed, at which point the state should befulfilledFailure state
  • The contents of the file read or the cause of the failure need to be saved and used separatelyvaluereasonstorage
const ENUM = {
 PENDING: 'pending'.  FULFILLED: 'fulfilled'.  REJECTED: 'rejected'
}
class Promise {  constructor (executor) {  this.status = ENUM.PENDING // Default state  this.value = undefined // Save the successful value  this.reason = undefined // Save the cause of execution failure  / / success  const resolve = (value) = > {  if (this.status === ENUM.PENDING) {  this.status = ENUM.FULFILLED  this.value = value  }  }  / / fail  const reject = (reason) = > {  if (this.status === ENUM.PENDING) {  this.status = ENUM.REJECTED  this.reason = reason  }  }  // Execute immediately  executor(resolve, reject)  } } Copy the code

2.1.3 Exception catching

Because the Executor executor is passed in by the user, errors may occur during execution. In this case, try… catch… Exception catching occurs, and when an error occurs, reject is called to throw an error

class Promise {
 constructor (executor) {
    / /...
    // Exception catch
   try{
 // Execute immediately  executor(resolve, reject)  } catch (e) {  reject(e)  }  } } Copy the code

2.1.4 Implement then method

There is a THEN method on the instance returned by calling New Promise(). Then method requires the user to provide two parameters, which are onFulfilled after the successful implementation and onRejected after the failed implementation

  • This will be called when the state becomes a pityonFulfilledMethod and pass in the success value this.value
  • Called when the state changes to RejectedonRejectedMethod and pass in the cause of the failure this.reason
class Promise {
    constructor(executor) {
        // ...
    }
    then(onFulfilled, onRejected) {
 if (this.status == ENUM.FULFILLED) {  onFulfilled(this.value)  }  if (this.status == ENUM.REJECTED) {  onRejected(this.reason)  }  } } Copy the code

When an asynchronous operation is performed in an executor, the then method is executed in a pending state

Asynchronous operations, such as setTimeout, are macro tasks, while promise.then is a micro task. The micro task is executed before the macro task, so when the THEN method is executed, the promise state is still pending

At the same time, instance Promise can call then methods multiple times, so you need to save the collection of callback functions in all then methods and execute the saved callback functions when the asynchronous operation is complete (publish-subscribe model).

const promise = new Promise((resolve, reject) = > {
   setTimeout((a)= > {}, 2000)
})

promise.then(data= > {/ /... }, err => {})
 promise.then(data= > {/ /... }, err => {}) Copy the code

So, what we need to do is

  • Create two queuesonResolvedCallbacksonRejectedCallbacks, respectively store the corresponding successful and failed callback in the THEN method
  • Called when the asynchronous operation succeedsresolveFunction is executedonResolvedCallbacksEach successful callback in the queue
  • Called when an asynchronous operation failsrejectFunction is executedonRejectedCallbacksEach failed callback in the queue
class Promise {
    constructor(executor) {
        this.status = ENUM.PENDING
        this.value = undefined
        this.reason = undefined
 this.onResolvedCallbacks = [] // Success queue  this.onRejectedCallbacks = [] // Failure queue  // Successful callback  const resolve = (value) = > {  if (this.status === ENUM.PENDING) {  this.status = ENUM.FULFILLED  this.value = value  this.onResolvedCallbacks.forEach(cb= > cb()) // Relative to publication  }  }  // Failed callback  const reject = (reason) = > {  if (this.status === ENUM.PENDING) {  this.status = ENUM.REJECTED  this.reason = reason  this.onRejectedCallbacks.forEach(cb= > cb())  }  }  // Execute immediately  executor(resolve, reject)  }  then(onFulfilled, onRejected) {  // ...  if (this.status === ENUM.PENDING) {  // Relative to subscription  this.onResolvedCallbacks.push((a)= > {  // todo...  onFulfilled(this.value)  });  this.onRejectedCallbacks.push((a)= > {  // todo...  onRejected(this.reason);  })  }  } } Copy the code

Note: In the THEN method, the callback function is not inserted directly into the queue. Instead, the function is wrapped and then pushed, which is convenient for subsequent expansion (eg: The return value of ondepressing () is captured and processed).

So far, we’ve implemented the basic Promise, but it looks like the callback is written differently than the previous one, and it doesn’t show any Promise advantages. Next, we’ll continue to explore advanced features in Promises

2.2 Advanced Features

2.2.1 Implement then chain call

For the THEN (onFulfilled, onRejected) method on the instance, its parameters are success and failure callback functions. The following usage scenarios are summarized

  • If both methods execute, the return value isCommon valuesIs passed to the next one in the outer layerthen
  • If two methods are executed duringAn exception is thrown, will be the nextthenCatch the exception in the failure callback
  • When both methods are executed, the return value ispromise, then the state of the promise is used as the result (the state of the promise is”successful“, the next one is calledthenThe successful callback; Status to”failure“Will call the next onethenFailed callback)
  • Error handling, when an error occurs (thenCast an error or return a failedpromise), which is caught by the most recent failed callback and can be continued after the failed callback executesthenmethods

In Promise, the Promise. Then chain call is implemented by returning a new Promise

Why did the “thought question” return a new promise instead of using the original promise?

Because the state of a promise cannot be changed once it has been “successful” or “failed”, a new promise must be returned so that success/failure callbacks in the next THEN can be continued

Next, you need to implement the following

  • callthenMethod to create a new onepromiseAnd finally put this newpromisereturn
  • Need to getthenIn the methodonFulfilled,onRejectedThe callback function returns the value through the newpromisePass on to the next onethenIn the method
class Promise {
    //....
    then(onFulfilled, onRejected) {
       / / a new promise
       let promise2 = new Promise((resolve, reject) = > {})
 if (this.status == ENUM.FULFILLED) {  let x = onFulfilled(this.value)  }  if (this.status == ENUM.REJECTED) {  let x = onRejected(this.reason)  }  if (this.status === ENUM.PENDING) {  this.onResolvedCallbacks.push((a)= > {  let x = onFulfilled(this.value)  });  this.onRejectedCallbacks.push((a)= > {  let x = onRejected(this.reason);  })  }  return promise2  } } Copy the code

Now, we need to pass the return value x from the callback function to the next THEN method. Is it a success callback or a failure callback from the next THEN method? You have to look at x.

  • ifxIs a normal value and will passpromise2In theresolvePassed to the success callback;
  • ifxIf it is an Error, it passespromise2In therejectPass to the failure callback;
  • Of course, x could also be a Promise instance, so you need to take that into account.

Since x is passed in resolve, reject in promise2 (both methods are not available externally) and new Promise(executor) is executed immediately, The resolve, reject methods are accessed by putting the entire then logic into an executor function

class Promise {
    //....
    then(onFulfilled, onRejected) {
        / / a new promise
        let promise2 = new Promise((resolve, reject) = > {
 if (this.status == ENUM.FULFILLED) {  // This is a big pity. catch... capture  try{  let x = onFulfilled(this.value)  resolve(x)  } catch (e){  reject(e)  }  }  // ...  })  return promise2  } } Copy the code

Because there are multiple cases for the return value x, the judgment logic is pulled away from the external function resolvePromise

class Promise {
    //....
    then(onFulfilled, onRejected) {
        / / a new promise
        let promise2 = new Promise((resolve, reject) = > {
 if (this.status == ENUM.FULFILLED) {  try{  let x = onFulfilled(this.value)  resolvePromise(x, promise2, resolve, reject)  } catch (e){  reject(e)  }  }  // ...  })  return promise2  } } const resolvePromise = (x, promise2, resolve, reject) = > {  } Copy the code

As you may have noticed, accessing Promise2 while the new Promise has not yet finished is a guaranteed error. Simply make resolvePromise into asynchronous code execution to access Promise2

/ /...
if (this.status == ENUM.FULFILLED) {
    setTimeout((a)= > {
        try {
            let x = onFulfilled(this.value)
 resolvePromise(x, promise2, resolve, reject)  } catch (e) {  reject(e)  }  }, 0) } Copy the code

Next, you need to implement the resolvePromise method

2.2.2 resolvePromise method

The resolvePromise method is intended to resolve whether X is A promise, according to Promises/A+ : the promise-resolution-procedure

ResolvePromise (x, promise2, resolve, reject)

  • (1) If x and promise2 reference the same object, an error is reported. (Sample code below)
let promise = new Promise((resolve, reject) = > {})

let promise2 = promise.then((a)= > {
 return promise2 // x represents the return value of the function in then, promise2
})
 promise2.then((a)= > {}, err=> {  console.log('err:', err) })  // Err: TypeError: Chaining cycle detected for Promise #
                 Copy the code
  • If (2)xIs a normal value, directly throughresolvereturn
  • If (3)xIs an object or a functionxIf there is athenMethod, when there isthenMethod, indicating thatxIs apromise, executethenmethods
  • (4)thenMethod, has a success callback and a failure callback, executes a success callback and passes in a success resulty; Execute the failed callback and pass in the failure causee, the use ofrejectreturn
  • (5) The return value y may still be a promise after successful execution, and continue to recursively parse the value of Y
  • (6) thenThe callback function can only be executed once, either on success or failure (set identifiercalled)
  • (7) whenxThere is nothenMethod, indicating thatxIt’s a normal object, straight throughresolvereturn
const resolvePromise = (x, promise2, resolve, reject) = > {
  / / (1)
  if (x === promise2) {
    reject(new TypeError(`TypeError: Chaining cycle detected for promise #<Promise>`))
  }
  if ((typeof x === 'object'&& x ! = =null) | |typeof x === 'function') {  let called = false / / (6)  try {  const then = x.then  / / (3)  if (typeof then === 'function') {  / / (4)  then.call(x, y => {  // (5) y could be a promise  if (called) return  called = true  resolvePromise(y, promise2, resolve, reject)  }, e => {  if (called) return  called = true  reject(e)  })  } else {  / / (7)  resolve(x)  }  } catch (e) {  // then the execution process fails, and the execution cannot continue  if (called) return  called = true  reject(e)  }  } else {  / / (2)  resolve(x)  } } Copy the code

Now that the resolvePromise method is basically implemented, the following points need to be noted

  1. Why is x a function?

Because resolvePromises need to be compatible with promises written by others, someone else’s promise could be a function

  1. performconst then = x.thenWhy use ittry... catch...Catch an exception?

The return value of x. teng can be overwritten using Object.defineProperties or Proxy

  1. performthenMethod, why usecall“Rather than directly executingx.then() ?

You can reuse the last then method to avoid calling x.teng () twice.

2.2.3 value through

new Promise((resolve, reject) = > {
 resolve(123)
}).then().then().then(data= > {
 console.log('success:', data)
})
// success: 123 Copy the code

How does 123 in the code above directly penetrate into the last THEN method?

Promises/A+ specification: onFulfilled, onRejected are optional arguments, stipulation then method onFulfilled, onRejected is optional parameter, so we need to provide A default value. Promises/A+ specification: onFulfilled, onRejected are optional arguments, stipulation then method onFulfilled, onRejected is optional parameter, so we need to provide A default value

class Promise {
    // ...
    then(onFulfilled, onRejected) {
        onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : v= > v
       onRejected = typeof onRejected === 'function' ? onRejected: e= > {throw e}
 // ...  } } Copy the code

This can be achieved by setting the default value of onFulfilled, onRejected. Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+ : Promises/A+

2.3 Specification Test

To standardize the test, you need to install the promises- Aplus-Tests NPM package first and add the following test code before exporting the Promise

class Promise {
 // ...
}
Promise.defer = Promise.deferred = function () {
    let dfd = {};
 dfd.promise = new Promise((resolve,reject) = >{  dfd.resolve = resolve;  dfd.reject = reject;  });  return dfd; } module.exports = Promise; Copy the code

Install dependencies

npm install promises-aplus-tests -D
Copy the code

Also add to package.json

"scripts": {
    "test": "promises-aplus-tests ./index.js"
 },
Copy the code

Finally, runnpm run testThe test results are as follows

2.4 Other methods and attributes

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

Against 2.4.1 catch method

The catch method on the instance is used to catch errors generated during execution and returns a promise as a failed callback, as opposed to then(null, onRejected).

class Promise{
  // ...
 catch (onErrorCallback) {
   return this.then(null, onErrorCallback)
  }
} Copy the code

The finally 2.4.2 method

Finally’s argument is a callback function that is executed whether a promise succeeds or fails.

Application scenarios are as follows: A page asynchronously requests data. No matter whether the data request succeeds or fails, loading is disabled in the finally callback function.

Also, the finally method has the following characteristics

  • Value through. I can put the frontpromiseIs passed to the nextthenMethod, or pass the error to the nextcatchIn the method
  • Waiting for. whenfinallyThe callback returns a new onepromise.finallyWill wait for thepromiseThe value is not processed until the execution is complete
  • If thepromiseSuccessful execution,finallyMethod will ignore the execution result or pass the result of the previous one to the nextthen
  • If the newpromiseAn error is reported when execution fails.finallyMethod passes the cause of the error to the nextcatchmethods

The following is a code demonstration

// The (1) value penetrates. Note that the finally callback has no arguments
Promise.resolve(100).finally((data) = > {
 console.log('finally: ', data)
}).then(data= > {
  console.log('success: ', data)
}).catch(err= > {  console.log('error', err) }) // finally: undefined // success: 100  // (2) Wait for execution // Returns a successful execution of the promise, but passes down the result of the last execution Promise.resolve(100).finally((a)= > {  return new Promise((resolve, reject) = > {  setTimeout((a)= > {  resolve(200)  }, 1000)  }) }).then(data= > {  console.log('success: ', data) // success: 100 }).catch(err= > {  console.log('error', err) })  // When a promise fails, the promise execution result is passed down Promise.reject(100).finally((a)= > {  return new Promise((resolve, reject) = > {  setTimeout((a)= > {  reject(200)  }, 1000)  }) }).then(data= > {  console.log('success: ', data) }).catch(err= > {  console.log('error', err) // error 200 })  Copy the code

Once you’ve mastered the use of finally, explore how to implement it.

class Promise{
 finally (callback) {
   return this.then(value= > {
     return Promise.resolve(callback()).then((a)= > value)
    }, err => {
 return Promise.resolve(callback()).then((a)= > {throw err})  })  } } Copy the code

2.4.3 Static method

Static methods are methods that are invoked through promises, not through instance promises

  • Promise.resolve(), promise.reject () return values: a Promise in a successful state, a Promise in a failed state
class Promise{
  // ...
  // Success status
 static resolve(value){
   return new Promise((resolve, reject) = > {
 resolve(value)  })  }  // Failed state  static reject(reason){  return new Promise((resolve, reject) = > {  reject(reason)  })  } } Copy the code

If a successful execution returns a value that is a promise, promise.resolve () will resolve the value recursively until the promise execution ends

class Promise{
 constructor() {
   / /...
    const resolve = (value) = > {
     if (value instanceof Promise) {
 // Parse recursively until value is normal  value.then(resolve, reject)  }  // ...  }  const reject = (err) = > {  // ...  }  / /...  } } Copy the code

Now, execute the following code and get the data as normal

Promise.resolve(new Promise((resolve, reject) = > {
 setTimeout((a)= > {
   resolve('hello')
  }, 2000)
})).then(data= > {
 console.log(data) // hello }) Copy the code
  • Promise.all()

Solve concurrency problems, multiple asynchronous concurrency and get the final result.

The argument is an array of promises, and if each item in the array succeeds, the result is a success, and if there is a failure, the result is a failure.

class Promise {
    static all(arrList) {
        if (!Array.isArray(arrList)) {
            const type = typeof arrList;
            return new TypeError(`TypeError: ${type} ${arrList} is not iterable`)
 }  return new Promise((resolve, reject) = > {  const backArr = []  const count = 0  const processResultByKey = (value, index) = > {  backArr[index] = value  if (++count === arrList.length) {  resolve(backArr)  }  }  for (let i = 0; i < arrList.length; i++) {  const item = arrList[i];  if (item && item.then === 'function') {  item.then((value) = > {  processResultByKey(value, i)  }, reject)  } else {  processResultByKey(item, i)  }  }  })  } } Copy the code

⚠️ Note: in the all method, ++count === arrlist.length (count == counter) is used to check whether all the execution is complete, instead of index === arrlist.length -1

// p1 is an instance of Promise
Promise.all([1.2, p1, 4]).then(data= > {})

// Index === arrlist. length-1 is valid when executing the last item of the array.
// execute resolve
// It is possible that p1 has not yet been executed, so use the counter to determine Copy the code
  • Promise.race()

Unlike the all method, promise.race takes the first success or first failure as an execution result

class Promise {
    static race(arrList) {
        return new Promise((resolve, reject) = > {
            for (let i = 0; i < arrList.length; i++) {
                const value = arrList[i];
 if (value && value.then === 'function') {  value.then(resolve, reject)  } else {  resolve(value)  }  }  })  } } Copy the code

The main application scenarios of promise.race are as follows

  • (Basic) Multiple requests take the fastest (eg: multiple agent lines of small aircraft, which line has the fastest response speed, which line will be used)
  • (seniorEncapsulate interrupt method, interruptpromise(Set timeout for asynchronous requests, when the timeout, asynchronous requests are forced to fail)

Native Promises do not have abort methods, assuming the following usage scenario

const p1 = new Promise((resolve, reject) = > {
 setTimeout((a)= > { // Simulate an asynchronous request and return after 5 seconds
    resolve('hello')
  }, 5000)
})
 const newP = wrap(p1) setTimeout((a)= > { // Set timeout. After timeout, newp. abort is called  newP.abort('Request timed out') }, 4000)  newP.then(data= > {}).catch(err= > {}) Copy the code

NewP1 is a promise with an abort method that is called newp.abort () after timeout.

Now you need to implement the wrap wrapper method, passing in a plain Promise instance and returning a Promise instance with an ABORT method

const wrap = (promise) = > {
  let abort
 let newPromise = new Promise((resolve, reject) = > {
   abort = reject
  })
 let p = Promise.race([promise, newPromise])  p.abort = abort  return p } Copy the code

The wrap method takes advantage of the promise.race feature that uses the fastest execution result to see which Promise, newPromise, executes first, and the execution of newPromise is done externally by calling abort

Iii. Extension of Promise

⚠️ Note: The following extensions to Promise only work in Node environments

3.1 promisify

Function: Convert an API in Node to write a promise, using fs.readfile as an example

Regular writing

const fs = require('fs)
fs.readFile('./name.json', (err, data) => {})
Copy the code

Cons: Callback hell nested

Promisify chain call

const util = require('util')

const read = util.promisify(fs.readFile)
read('./name.json').then(data= > console.log(data))
Copy the code

Features: The promisify method has the following features

  • Returns a function that returns a promise only after it executes
const promisify = fn= > {
 return (. args) = > {
   return new Promise((resolve, reject) = > {
fn(... args, (err, data) => {       if (err) reject(err)
 resolve(data)  })  })  } } Copy the code
  • In the promisify function, the callback function can be manually added when the FN function is executed because most of the node method callback is in this format

3.2 bluebird

The promisify method can only modify one method at a time, but the third-party library Bluebird implements the promisifyAll method, which can convert all methods under an object into a promise

const fs = require('fs')
const bluebird = require('bluebird'); // Third-party library, need to be installed in advance
const newFs = bluebird.promisifyAll(fs);
newFs.readFileAsync('./name.txt'.'utf-8').then(data= > {}).catch(err= > {})
Copy the code

PromisifyAll () has the following features

  • The function takes an object as an argument, and adds an Async** ** suffix to all methods on the object to make them promise
  • It does not override the original method, but merely extends it
const promisifyAll = (target) {
 Reflect.ownKeys(target).forEach(key= > {
    target[`${key}Async`] = promisify(target[key])
  })
  return target
} Copy the code

Reflect Object is an Object embedded in the ES, it provide intercepts JavaScript operation methods Reflect | MDN, here, also can use the Object. The keys (). Meanwhile, the method is overwritten using the previous promisify

3.3 Native Node support

Currently, in older browsers, the API has been integrated with the promise writing, using the following

const fs = require('fs').promises
fs.readFile('./name.txt'.'utf-8').then(data= > {})
Copy the code

Some third-party extensions are no longer popular because of native support

Iv. Reference resources

  • Promises/A+
  • The most detailed handwritten Promise tutorial ever – Carus
  • 45 Promise interview questions: One good one – LinDaiDai_ Lin

After five, language

This article is formatted using MDNICE