Promise, as a new feature in the ES6 series, is undoubtedly a top priority in front-end development. In 2021, Promise is everywhere in both open source projects and business code. This article rewrites A complete version of Promise and its static methods based on the A+ specification to deepen my understanding of asynchronous code.

Write a pre –

It’s all written code, so you must have used it a lot and be familiar with it. Here’s a review of the Promise syntax and static methods.

Grammar knowledge

  1. Create it directly through new (constructor) or static methods such as promise.resolve ()
  2. There are only three states (Pending [initial waiting state].Depressing [state of success].Rejected [failed state])
  3. Once the Promise status is confirmed,irreversible. Cannot be modified again
  4. Chain calls through the then/catch/finally methods all return new promises
  5. The two callback parameters resolve and reject are the success and failure callbacks, respectively
  6. Perform then (..) Register callback processing arrays (then methods can be called multiple times by the same Promise)
  7. The essence is to execute in a microtask queue

(Flow chart of Promise execution from MDN)

Methods:

  • Prototype. Then (onFulfilled, onRejected) adds a successful or failed callback to the current Promise and returns a new Promise

  • Promise.prototype. Catch (onRejected) adds the failed callback to the current Promise and returns a new Promise

  • Promise. Prototype. Finally (onFinally) add a callback to the current Promise (regardless of success or failure)

Static methods (all return new Promises) :

  • Promise.resolve(value) : Returns a Promise object with a status of success and passes the given success information to the corresponding handler

  • Promise.reject(reason) : Returns a Promise object in a failed state and passes the given failure information to the corresponding handler

  • Promise.all(iterable) : Success is triggered when all of the Promise objects in the iterable argument succeed, or if one of them fails

  • Promise. AllSettled (Iterable) : Iterable parameter Return after all Promises are complete (including success and failure)

  • Promise.any(iterable) : Receives a collection of Promise objects and returns the value of that successful Promise when one of them succeeds

  • Promise.race(iterable) : Receives a collection of Promise objects and returns the Promise value when one Promise succeeds or fails

Handwritten code

Es6 syntax is used here to build promises

The basic structure

We know that Promise has three states, defined in terms of static constants

const PENDING = "pending";
const RESOLVE = "fulfilled";
const REJECT = "rejected";
Copy the code

Use the class syntax to define a Promise and initialize the state. Since the then method of the same Promise can be called multiple times, the failed and successful callbacks are arrays. When a new Promise is called, a function is passed that has two methods: the successful and failed callback. We execute the incoming callback by defining an Executor. If an error is passed in, the error is caught directly with a try catch and returned with a reject message.

class Promise {
  constructor(execurte) {
    // The default state is wait
    this.status = PENDING;
    // Default value for successful callback
    this.value = undefined;
    // Default value for failed callback
    this.resaon = undefined;
    // The successful callback queue can be multiple times then, so there are multiple defined as arrays
    this.resolveCallBacks = [];
    // Failed callback
    this.rejecteCallBacks = [];
    // Exception handling for actuators
    try {
      execurte(this.resolve, this.reject);
    } catch (error) {
      this.reject(error); }}}Copy the code

With the basic structure written, we can see that we are ready for the first step of creation using the following example code.

let promise = new Promise((resolve, reject) = > {
  // executor (" Hello Promise")
});
Copy the code

During executor execution. We need to process the contents of resolve, reject, and process the state and data. If the current state is pending (that is, the initialization state), then the corresponding state we modify is success or failure. Save the success or failure values and execute them in the callback queue. With queueMicrotask we perform microtasks

resolve = (value) = > {
  queueMicrotask(() = > {
    if (this.status === PENDING) {
      this.status = FULFILLED; // Change the status
      this.value = value;
      this.resolveCallBacks.forEach((fn) = > fn(this.value)); // Successful callback}}); };// Callback on failure
reject = (resaon) = > {
  queueMicrotask(() = > {
    if (this.status === PENDING) {
      this.status = REJECTED; // Change the status
      this.resaon = resaon;
      this.rejecteCallBacks.forEach((fn) = > fn(this.resaon)); // Failed callback}}); };Copy the code

At this point, the basic Promise structure is done, and then the all-important chain call is implemented

Chain calls

We know that promises implement chained calls through then, so to summarize the rules:

  1. Each then returns a new Promise
  2. Accept two parameters, both of type Function, to be called when Promise changes state. The first parameter will run in the resolved state and accept the result, and the second parameter will run in the Rejected state and accept the result
  3. In addition to receiving two function arguments, the then can continue after the then
  4. Then may return a normal value or a Promise

In the then process, we may directly return a normal value, such as return 100, or we may return a Promise, such as return new Promise(), so we define a utility function _returnValue to handle the returned value. Note that one of the possible scenarios is as follows. Creating the current object and then returning the current object’s condition then creates a circular reference. So you need to add a logical processing of circular references.

// Cyclic reference test
let p1 = new MyPromise((resolve, reject) = > {
  resolve("yes");
});
let p2 = p1.then((res) = > {
  return p2;
});
// From this then, a circular reference is generated
p2.then(
  () = > {},
  (reson) = > {
    console.log(reson); });Copy the code

You then get a utility function that handles the return value, _returnValue

/ * * * *@param {*} P Currently running Promise *@param {*} CallbackValue Returns the value (then) *@param {*} Resolve Successful callback *@param {*} Reject Failed callback *@returns* /
_returnValue(p, callbackValue, resolve, reject) {
      // If p is equal to callbackValue, a circular reference is generated
      if (p === callbackValue) {
          return reject(new TypeError('Pretty boy, your code is looping'))}// Check whether callbackValue is a Promise type
      if (callbackValue instanceof MyPromise) {
          callbackValue.then(value= > resolve(value), resaon= > reject(resaon))
      } else {
          resolve(callbackValue)
      }
}
Copy the code

With utility functions, chain calls to implement THEN can directly handle the results. This method takes two of the previously mentioned function arguments, and if no null value is passed to us, returns a function directly, or throws an error for subsequent THEN procedures. There are three states in the processing of THEN

  • The successful state
  • The failure state
  • Wait state

For both success and failure states, we simply execute the corresponding callback function or catch exception handling. Return the value directly using the _returnValue defined earlier.

ResolveCallBacks and rejecteCallBacks store tasks that need to be executed. If the status is pending, the callback is returned asynchronously. Resolve or Reject is not called yet. Join the microtask queue, wait for the result to return and then execute the desired callback function. Without this step, the synchronous code will complete the Promise directly when an asynchronous task is present in the Promise, without changing the state of the Promise after the asynchronous task is complete.

then = (resolveCallBack, rejecteCallBack) = > {
  // If null is passed, the default is passed backwards, so add a default case
  resolveCallBack = resolveCallBack ? resolveCallBack : (value) = > value;
  // Parameters are optional
  rejecteCallBack = rejecteCallBack ? rejecteCallBack : (reason) = > { throw reason; };
  let p = new MyPromise((resolve, reject) = > {
    // Handle different returns, either a normal value directly or a Promise object for further calls
    / / success
    if (this.status === FULFILLED) {
      // Start a microtask and wait for p result to return. Otherwise the program will return the value of p
      // Exception handling for executed functions
      queueMicrotask(() = > {
        try {
          let callbackValue = resolveCallBack(this.value);
          this._returnValue(p, callbackValue, resolve, reject);
        } catch(error) { reject(error); }});/ / fail
    } else if (this.status === REJECTED) {
      queueMicrotask(() = > {
        try {
          let callbackValue = rejecteCallBack(this.resaon);
          this._returnValue(p, callbackValue, resolve, reject);
        } catch(error) { reject(error); }});// The waiting process
    } else {
      // The task is stored and then executed
      // Store successful tasks
      this.resolveCallBacks.push(() = > {
        queueMicrotask(() = > {
          try {
            let callbackValue = resolveCallBack(this.value);
            this._returnValue(p, callbackValue, resolve, reject);
          } catch(error) { reject(error); }}); });// Storage failure
      this.rejecteCallBacks.push(() = > {
        queueMicrotask(() = > {
          try {
            let callbackValue = rejecteCallBack(this.resaon);
            this._returnValue(p, callbackValue, resolve, reject);
          } catch(error) { reject(error); }}); }); }});return p;
};
Copy the code

Catch is easy to implement once you’ve done then, calling the THEN method directly and passing in a failed function.

// Register a non-static method to catch error information
catch(rejecteCallBack) {
    return this.then(undefined, rejecteCallBack)
}
Copy the code

A finally method is a method that executes whether it succeeds or fails, returning a method directly through THEN

// Register a non-static method, finally executed whether it succeeds or fails
finally(callback) {
    return this.then((value) = > {
        return MyPromise.resolve(callback()).then(() = > value)
    }, (resaon) = > {
        return MyPromise.resolve(callback()).then(() = > { throw resaon })
    })
}
Copy the code

A static method

resolve

This just returns a Promise of success. Nothing to say

static resolve(value) {
    // If it is a Promise object, return it directly
    if (value instanceof MyPromise) {
        return value
    } else {
        // If it is not a Promise object, create another one
        return new MyPromise((resolve) = > {
            resolve(value)
        })
    }
}
Copy the code

reject

Return a failed Promise

    // static method, return false Promise
    static reject(resaon) {
        if (resaon instanceof MyPromise) {
            return resaon
        } else {
            // If it is not a Promise object, create another one
            return new MyPromise((resolve, reject) = > {
                reject(resaon)
            })
        }
    }
Copy the code

all

All mainly keeps the results of each one and returns when all the results are complete. Then is used to obtain the Promise of success

static all(promises) {
    // Saves an array of callback results
    let result = [];
    // an accumulator is used to determine whether the execution of the method queue is complete
    let count = 0;
    // The all method also returns a Promise object
    return new MyPromise((resolve, reject) = > {
        function pushResult(key, value) {
            result[key] = value
            count++
            // If the accumulator and the list of tasks executed are equal in length, the entire task has been completed
            if (count === promises.length) {
                resolve(result)
            }
        }
        // Loop through the tasks to be executed
        promises.forEach((task, index) = > {
            if (task instanceof MyPromise) {
                task.then((v) = > pushResult(index, v), (resaon) = > reject(resaon))
            } else {
                pushResult(index, promises[index])
            }
        })
    })
}
Copy the code

allSettled

AllSettled will return to you regardless of success or failure, so use the finally keyword directly when processing. If it is a common type, save value

static allSettled(promises) {
    return new MyPromise((resolve) = > {
        let results = []
        let count = 0
        promises.forEach((task, index) = > {
            if (task instanceof MyPromise) {
                task.finally(_= > {
                    count++
                    results[index] = {
                        status: task.status,
                        value: task.value || task.resaon
                    }
                    if (count === promises.length) {
                        resolve(results)
                    }
                })
            } else {
                count++
                results[index] = {
                    status: 'fulfilled'.value: task
                }
                if (count === promises.length) {
                    resolve(results)
                }
            }
        })
    })
}
Copy the code

any

Return as long as there is a success, again using then

static any(promises) {
    return new MyPromise((resolve) = > {
        promises.forEach((task) = > {
            if (task instanceof MyPromise) {
                task.then(_= > {
                    resolve(task.value)
                })
            } else {
                resolve(task)
            }
        })
    })
}
Copy the code

race

As long as there is a success or return, use finally to get

static race(promises) {
    return new MyPromise((resolve) = > {
        promises.forEach((task) = > {
            if (task instanceof MyPromise) {
                task.finally(_= > {
                    resolve(task.value || task.resaon)
                })
            } else {
                resolve(task)
            }
        })
    })
}
Copy the code

Adding a Test Configuration

Then add the defer use case and execute the test case

promises-aplus-tests Promise.js 
Copy the code
MyPromise.defer = MyPromise.deferred = function () {
    let testObj = {}
    testObj.promise = new Promise((resolve, reject) = > {
        testObj.resolve = resolve
        testObj.reject = reject
    })
    return testObj
}
module.exports = MyPromise
Copy the code

The complete code

Complete code GitHub address