preface

Promise is a necessary repository of knowledge for every front-end, and promises can solve the common problems of nested callbacks, synchronous concurrency, and multiple asynchronous processing errors. We can make asynchronous methods more elegant by wrapping them in promises, while making asynchronous implementations of async-await synchronous execution based on promises. Understanding how Promise works can reduce some of the confusion and make it easier to use.

Some knowledge points need to be lit up before writing

  • Javascript basics (prototypes, closures, type determination… , I will not elaborate here.)
  • What is a higher-order function?
  • Do you know what publishing subscription is?

Higher-order functions

A higher-order function is one that returns another function, or one that has a function as an argument.

In the business development, some core functions with strong generality need to be added some additional logic, so we can use higher-order functions for wrapping processing.

// Core method
function core(. args){
    // code omitted....
    console.log('core',args);
    // code omitted....
}
// Add some extra logic to the core function without changing the core code
Function.prototype.before = function (cb) {
    return (. args) = >{
        cb();
        this(...args);
    }
}
let newCore = core.before(() = >{
    console.log('before')
})
newCore('a'.'b');
Copy the code

We extend the function through higher order functions.

Release subscription

Publish subscription is divided into two parts: ON and EMIT. On maintains some functions in an array and emit executes the methods in the array. On (subscribe) and EMIT (publish) are not clearly related. Let’s illustrate this with a specific scenario.

Now I need to read two files separately. I need to print the contents of both files after reading them. When should I print the contents?

You might think of a variable as a counter, and when the initial value of the counter is 2, when the counter is 0 it means that both files have been read, and then we can do the logic.

let fs = require('fs');
let person = {};

function after(time, callback) {
  return function() {
    if (--time == 0) { callback(); }}}let cb = after(2.function() {
  console.log(person)
})

fs.readFile('./promise/callback/age.txt'.'utf-8'.function (err, data) {
  person.age = data;
  cb()
})

fs.readFile('./promise/callback/name.txt'.'utf-8'.function (err, data) {
  person.name = data;
  cb()
})
Copy the code

But if we want to do more at this point, we may need to constantly define new after1, after2… And CB1, CB2… , and keep calling it in readFile. It would be cumbersome and not elegant to use. At this time, we can achieve more flexibility through the publish and subscribe mode. We can manage our business functions through the array. On puts the function into the array, emit traverses the number group, and only call the emit method once for each asynchronous file reading.

let fs = require('fs');
let person = {};
let event = { 
  arr: [].on: function(fn) {
    this.arr.push(fn)
  },
  emit: function() {
    this.arr.forEach(item= > item())
  }
}

event.on(function() {
  console.log("In reading data...")
})

event.on(function() {
  if (Object.keys(person).length === 2) {
    console.log(person)
  }
})

fs.readFile('./promise/callback/age.txt'.'utf-8'.function (err, data) {
  person.age = data;
  event.emit();
})

fs.readFile('./promise/callback/name.txt'.'utf-8'.function (err, data) {
  person.name = data;
  event.emit();
})
Copy the code

Make a simple Promise

To implement a simple Promise, first we need to analyze the basic use of the Promise. By new a Promise(perform function), we can get an instance of the Promise Promise. Two parameters are passed in the execution function. By calling these two parameters, we can change the state of the promise from the wait state to the success/failure state. We can then perform logical processing on success/failure by referring to two functions in the promise’s then method.

const FULFILLED = 'fulfilled'; / / success
const REJECTED = 'rejected'; / / fail
const PENDING = 'pending'; / / wait for
class Promise {
  constructor(executor) {
    this.status = PENDING; // Promise default state
    this.value - undefined; // Success value
    this.reason = undefined; // Cause of failure
    const resolve = (value) = > { // The resolve function succeeds
      if (this.status === PENDING) {
        this.value = value;
        this.status = FULFILLED; // Change the status}}const reject = (reason) = > { // Reject fails
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED; // Change the status}}try {
      executor(resolve, reject); // Execute immediately
    } catch(e) {
      reject(e)
    }
  }
  then(onFulfilled, onRejected) {
    if (this.status === FULFILLED) {
      onFulfilled(this.value)
    }
    if (this.status === REJECTED) {
      onRejected(this.reason)
    }
  }
}
Copy the code

Create a Promise class, pass an executor in its constructor, place Status (the state of the Promise), value(the success value), and Reason (the failure reason) on the Promise instance, and define the success function resolve, Reject and reject. As the state of Promise cannot be changed once it is changed, it is necessary to judge the status inside the successful and failed function, update the value of status and assign value and reason accordingly. Since the Promise executor executes immediately, we let it execute in the constructor, and if an exception occurs while executing the function, we use a try catch to execute reject. We have completed the Promise creation. Now we will implement the THEN method. Add a new THEN method inside the Promise class, which will pass in two callback functions: onFulfilled and onRejected. Execute the corresponding callback function.

Promise asynchronous problem

Now that we have implemented a simple promise, what if status hasn’t changed from pending to another state when we call the promise’s then method?

Promise.resolve(new Promise((resolve, reject) = > {
  setTimeout(() = > {
    resolve(200)},3000)
})).then(res= > {
  console.log(res)
}, err= > {
  console.log(err)
})
Copy the code

In order to solve this problem, we need to handle status equals pending in THEN, which requires the idea of publish and subscribe. When calling THEN, if the status is pending, Thus, we can store the current callback function in the corresponding array of success/failure callbacks. When the state changes from Pending to pity/Rejected, the function in the array will be iterated.

const FULFILLED = 'fulfilled'; / / success
const REJECTED = 'rejected'; / / fail
const PENDING = 'pending'; / / wait for
class Promise {
  constructor(executor) {
   	// code omitted...
    this.fulfilledCallback = []; // Successful callback
    this.rejectedCallback = []; // Failed callback
    const resolve = (value) = > { // The resolve function succeeds
      if (this.status === PENDING) {
        // code omitted...
        this.fulfilledCallback.forEach(item= >{ item() }); }}const reject = (reason) = > { // Reject fails
      if (this.status === PENDING) {
       // code omitted...
        this.rejectedCallback.forEach(item= >{ item() }); }}// code omitted...
  }
  // When the user calls the then method, the promise may be in a wait state, stored first, because resolve, reject may be called later
  then(onFulfilled, onRejected) {
    if (this.status === PENDING) { // Then is asynchronous
      this.fulfilledCallback.push(() = > { 
        onFulfilled(this.value);
      })
      this.rejectedCallback.push(() = > {
        onRejected(this.reason); })}// code omitted...}}Copy the code

Chain calls to soul THEN

The chain calls to THEN are the soul of the Promise and the most difficult part of implementing a Promise. To implement chain calls to THEN, we first need to have a detailed understanding of how to use THEN.

  1. PromiseWhen the then method is called, a new one is returnedPromise.
  2. When a method succeeds or fails in then, it returns a normal value, noPromiseAs a result of the success of the outer next THEN.
  3. When the execution of the THEN Chinese method goes wrong, the result of the next then failure will be reached.
  4. If one is returned in thenPromiseObject, which will be based onPromiseThe results to deal with is to go success or failure.

In a nutshell: returning a normal value (with the exception of a Promise) will pass to the success of the next THEN, returning a failed Promise or throwing an exception will pass to the failure of the next THEN.

From the then chain we can easily write what happens when the return value of the THEN is normal.

// code omitted...
class Promise {
  constructor(executor) {
    // code omitted...
  }
  // When the user calls the then method, the promise may be in a wait state, stored first, because resolve, reject may be called later
  then(onFulfilled, onRejected) {
    let promise = new Promise((resolve, reject) = > {
      if (this.status === PENDING) { // Then is asynchronous
        this.fulfilledCallback.push(() = > {
          try {
            let x = onFulfilled(this.value);
            resolve(x);
          } catch (error) {
            reject(error)
          }
        })
        this.rejectedCallback.push(() = > {
          try {
            let x = onRejected(this.reason);
            resolve(x);
          } catch (error) {
            reject(error)
          }
        })
      }
      if (this.status === FULFILLED) {
        try {
          let x = onFulfilled(this.value);
          resolve(x);
        } catch (error) {
          reject(error)
        }
      }
      if (this.status === REJECTED) {
        try {
          let x = onRejected(this.reason);
          resolve(x);
        } catch (error) {
          reject(error)
        }
      }
    })
    returnpromise; }}Copy the code

Wrap the logic we wrote in then with a new Promise, store the return value of the callback function with the variable X, and execute the Promise’s resolve(x). If an error occurs, use trycatch to catch and execute the Promise’s Reject method. Finally, return the promise.

Having solved the case where the then callback returns a normal value, we now need to solve the case where the return value is Promise:

// code omitted...
function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(new TypeError('wrong'));
  }
  if (typeof x === 'function'|| (x ! = =null && typeof x === 'object')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, res= > {
          resolvePromise(promise, res, resolve, reject);
        }, rej= >{ reject(rej); })}else{ resolve(x); }}catch(err) { reject(rej); }}else{ resolve(x); }}class Promise {
  constructor(executor) {
   // code omitted...
  }
  // When the user calls the then method, the promise may be in a wait state, stored first, because resolve, reject may be called later
  then(onFulfilled, onRejected) {
    let promise = new Promise((resolve, reject) = > {
      if (this.status === PENDING) { // Then is asynchronous
        this.fulfilledCallback.push(() = > {
          setTimeout(() = > {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0);
        })
        this.rejectedCallback.push(() = > {
          setTimeout(() = > {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0); })}if (this.status === FULFILLED) {
        setTimeout(() = > {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0);

      }
      if (this.status === REJECTED) {
        setTimeout(() = > {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0); }})returnpromise; }}Copy the code

At this point, we need to process the return value x of the callback function. If it is a normal value, we can simply call promise’s resolve. If the return value x is a Promise object, THEN I need to recursively process the Promise object and call the Promise’s resolve when the result is a normal value. Here we can split the x method out of resolvePromise, passing in promise, X, and resolve, reject. Since we’re going to use promise, promise hasn’t been defined yet, So we need a setTimeout to make it asynchronous. Now let’s deal with resolvePromise, which can be divided into the following points:

  1. Make a promise === x? Call reject.
  2. Check if x is function or if x is object and not null.
  3. If step 2 is false, x is a normal value, and you simply call the resolve method.
  4. If step 2 is true then the then property of x is determined.
  5. If then is not function, x is a normal value. Call resolve.
  6. If then is function we can say that x isPromiseCall the then method and use the call method to make its internal this point to X. Then’s success callback calls resolvePromise, replacing the second argument with the one passed in by the callback function. Call reject in the then failure callback.

By doing this, we have basically made A Promise that Promises/A+. 😊

Attach the complete code:

const FULFILLED = 'FULFILLED'; / / success
const REJECTED = 'REJECTED'; / / fail
const PENDING = 'PENDING'; / / wait for
function resolvePromise(promise, x, resolve, reject) {
  if (promise === x) {
    return reject(new TypeError('wrong'));
  }
  if (typeof x === 'function'|| (x ! = =null && typeof x === 'object')) {
    try {
      let then = x.then;
      if (typeof then === 'function') {
        then.call(x, res= > {
          resolvePromise(promise, res, resolve, reject);
        }, rej= >{ reject(rej); })}else{ resolve(x); }}catch(err) { reject(rej); }}else{ resolve(x); }}class Promise {
  constructor(executor) {
    this.status = PENDING; // Promise default state
    this.value - undefined; // The reason for success
    this.reason = undefined; // Cause of failure
    this.fulfilledCallback = []; // Successful callback
    this.rejectedCallback = []; // Failed callback
    const resolve = (value) = > { // The resolve function succeeds
      if (this.status === PENDING) {
        this.value = value;
        this.status = FULFILLED; // Change the status
        this.fulfilledCallback.forEach(item= >{ item() }); }}const reject = (reason) = > { // Reject fails
      if (this.status === PENDING) {
        this.reason = reason;
        this.status = REJECTED; // Change the status
        this.rejectedCallback.forEach(item= > {
          item()
        })
      }
    }
    try {
      executor(resolve, reject); // Execute immediately
    } catch (e) {
      reject(e)
    }
  }
  // When the user calls the then method, the promise may be in a wait state, stored first, because resolve, reject may be called later
  then(onFulfilled, onRejected) {
    let promise = new Promise((resolve, reject) = > {
      if (this.status === PENDING) { // Then is asynchronous
        this.fulfilledCallback.push(() = > {
          setTimeout(() = > {
            try {
              let x = onFulfilled(this.value);
              resolvePromise(promise, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0);
        })
        this.rejectedCallback.push(() = > {
          setTimeout(() = > {
            try {
              let x = onRejected(this.reason);
              resolvePromise(promise, x, resolve, reject)
            } catch (error) {
              reject(error)
            }
          }, 0); })}if (this.status === FULFILLED) {
        setTimeout(() = > {
          try {
            let x = onFulfilled(this.value);
            resolvePromise(promise, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0);

      }
      if (this.status === REJECTED) {
        setTimeout(() = > {
          try {
            let x = onRejected(this.reason);
            resolvePromise(promise, x, resolve, reject)
          } catch (error) {
            reject(error)
          }
        }, 0); }})returnpromise; }}module.exports = Promise
Copy the code
/ / test
let MyPromise = require('./source/4promise')
let promise = new MyPromise((resolve, reject) = > {
  setTimeout(() = > {
    resolve('success')},3000)
  console.log('开始')
})

promise.then((res) = > {
  console.log(res)
  return new MyPromise((resolve, reject) = > {
    reject('failure of 88888')})},err= > {
  console.log(err)
}).then((res) = > {
  console.log("---2res", res);
}, err= > {
  console.log("---2err", err);
  return { ss: "jinhong" };
}).then(res= > {
  console.log("-------3res", res);
}, err= > {
  console.log("-------3err", err);
}).then(res= > {
  console.log("-------4res", res);
}, err= > {
  console.log("-------4err", err);
})
Copy the code

Other methods

Resolve (value), Promise. Reject (value), and Promise. Reject (value) each create a Promise object with a success value and a rejection reason, respectively. And combinatorial tools that run asynchronous operations in parallel, promise.all () and promise.race (), and the error-catching catch method. These methods become correspondingly easier to implement once we understand the THEN chain calls.

  • Promise.resolve(value)andPromise.reject(value)

    These two static methods simply new a Promise and call the corresponding successful or failed method in the execution function.

    // code omitted...
    function resolvePromise(promise, x, resolve, reject) {
      // code omitted...
    }
    class Promise {
      // code omitted...
     	static resolve(value){
        return new Promise((resolve,reject) = >{ resolve(value); })}static reject(value){
        return new Promise((resolve,reject) = >{ reject(value); }}})Copy the code
  • catch

    Catch is the method on the Promise prototype, and the catch implementation mainly specifies a callback function that is passed as an argument to the failure callback of then.

    // code omitted...
    function resolvePromise(promise, x, resolve, reject) {
      // code omitted...
    }
    class Promise {
      // code omitted...
      catch(errorFn){
        return this.then(null,errorFn); }}Copy the code
  • finally

    Finally is executed on success or failure, and can continue downwards.

    // code omitted...
    function resolvePromise(promise, x, resolve, reject) {
      // code omitted...
    }
    class Promise {
      // code omitted...
      finally(cb) {
         return this.then((data) = > {
            return Promise.resolve(cb()).then(() = > data);
        }, (err) = > {
            return Promise.resolve(cb()).then(() = > { throwerr }); }}})Copy the code
  • Promise.all()

    Promise.all receives an array and waits for everything to complete successfully, returning a new array, in which if one fails, promise. all will immediately become a failure.

    // code omitted...
    function resolvePromise(promise, x, resolve, reject) {
      // code omitted...
    }
    class Promise {
      // code omitted...
      static all = function (promises) {
        return new Promise((resolve, reject) = > {
          let result = [];
          let times = 0;
          const processSuccess = (index, val) = > {
            result[index] = val;
            if(++times === promises.length) { resolve(result); }}for (let i = 0; i < promises.length; i++) {
            let p = promises[i];
            if (p && typeof p.then === 'function') {
              p.then((data) = > {
                processSuccess(i, data)
              }, reject)
            } else{ processSuccess(i, p); }})}}Copy the code
    Promise.all([ 
        new Promise((resolve, reject) = > {
        setTimeout(() = > {
            resolve('1' success)},2000);
    }), new Promise((resolve, reject) = > {
        setTimeout(() = > {
            resolve('2' success)},1000);
    }),1.2.3]).then(data= > {
        console.log(data);
    }).catch(err= > {
        console.log(err,'errr')})Copy the code
  • Promise.race()

    Promise.race() uses the results of whoever succeeds or fails first and can generally be used for timeout processing.

    // code omitted...
    function resolvePromise(promise, x, resolve, reject) {
      // code omitted...
    }
    class Promise {
      // code omitted...
      static race = function (promises) {
        return new Promise((resolve, reject) = > {
          for (let i = 0; i < promises.length; i++) {
            let p = promises[i];
            if (p && typeof p.then === 'function') {
              p.then(resolve, reject); // Stop when you succeed
            } else{ resolve(p); }})}}Copy the code
    / / test
    let p1 = new Promise((resolve,reject) = >{
        setTimeout(() = > {
            resolve('success')},1000);
    })
    
    let p2 = new Promise((resolve,reject) = >{
        setTimeout(() = > {
            reject('failure')},500);
    })
    Promise.race([p1, p2, null.1]).then((data) = > {
        console.log(data);
    }).catch((err) = > {
        console.log(err)
    });
    Copy the code

application

  • Image load timeout, script load timeout problem, no longer use successful results

    var abort;
    let p1 = new Promise((resolve, reject) = > {
      abort = reject;
      setTimeout(() = > {
        resolve('success');
      }, 3000);
    })
    p1.abort = abort;
    p1.then(data= > {
      console.log(data);
    }, err= > {
      console.log(err);
    })
    setTimeout(() = > {
      p1.abort("More than a second.");
    }, 1000);
    Copy the code

    It’s not a good idea to expose a variable like this, so we can take advantage of the race feature to new a timeout promise.

    function wrap(p) {
      let abort;
      let p1 = new Promise((resolve, reject) = > {
        abort = reject;
      })
      let p2 = Promise.race([p, p1]);
      p2.abort = abort;
      return p2;
    }
    let p1 = new Promise((resolve, reject) = > {
      setTimeout(() = > {
        resolve('success');
      }, 3000);
    })
    
    let p2 = wrap(p1);
    p2.then(data= > {
      console.log(data);
    }, err= > {
      console.log(err);
    })
    
    setTimeout(() = > {
      p2.abort("More than a second.");
    }, 1000);
    Copy the code