preface

Promises are a particularly important concept in front-end development, with many asynchronous operations relying on promises. Since we have dealt with it so much in daily life, let’s implement a Promise by ourselves, deepen our understanding of Promise, and enhance our JavaScript skills.

This time, jEST will be used to write test cases while realizing the Promise, so as to ensure the correctness of the implementation process.

If you want to see the construction of the test framework or the complete implementation, you can click on my Github warehouse to view, if you like, welcome star, if you find my mistakes, welcome to mention.

A + specification

This is an open, robust, and universal Promise implementation specification. Developed by developers for their reference.

Here is the official specification, according to the official specification to implement, you can write a Promise that belongs to your own, in line with the standard.

implementation

Without further ado, let’s start implementing promises according to the A+ specification.

The first section of the specification is an expression of terminology, with no actual functionality required to implement.

1. Declare the Promise class

Promise is a class (JavsScript’s class is implemented as a function, just a syntactic sugar) that must accept a function or report an error; It also has a then method.

class PROMISE {
  constructor(executor) {
    if (typeofexecutor ! = ='function') {
      throw new TypeError(`Promise resolver ${executor} is not a function`);
    }
  }
  then() {}
}
Copy the code

For example:

import Promise from '.. / ';

describe('Promise', () => {
  test('Is a class', () => {
    expect(Promise).toBeInstanceOf(Function);
    expect(Promise.prototype).toBeInstanceOf(Object);
  });
  test('New Promise() must accept a function', () => {
    expect((a)= > {
      // @ts-ignore
      new Promise(a); }).toThrow(TypeError);
    expect((a)= > {
      // @ts-ignore
      new Promise('promise');
    }).toThrow(TypeError);
    expect((a)= > {
      // @ts-ignore
      new Promise(null);
    }).toThrow(TypeError);
  });
  test('New Promise(fn) will generate an object that has then methods', () = > {const promise = new Promise((a)= > {});
    expect(promise.then).toBeInstanceOf(Function);
  });
})
Copy the code

I won’t list the test cases later, but you can check them out in my Github repository.

2. Maintain status

Promise has three states: pending, fulfilled and rejected. This is a big pity. A Promise starts out as a request, it can change into two other states (only allowed to change once), and it gets a value when the state changes.

class PROMISE {
  constructor(executor) {
    if (typeofexecutor ! = ='function') {
      throw new TypeError(`Promise resolver ${executor} is not a function`);
    }
    // Initial state
    this.status = 'pending';
    / / initial value
    this.value = null;
    // Class is in strict mode by default, so you need to bind this
    executor(this.resolve.bind(this), this.reject.bind(this));
  }
  then() {}
  resolve(value) {
    // State protection
    if (this.status ! = ='pending') {
      return;
    }
    // Change the state and assign
    this.status = 'fulfilled';
    this.value = value;
  }
  reject(reason) {
    // State protection
    if (this.status ! = ='pending') {
      return;
    }
    // Change the state and assign
    this.status = 'rejected';
    this.value = reason; }}Copy the code

At this point, we have almost implemented specification 2.1.

3.then

Then can accept two functions that will be executed asynchronously after a state change and, according to specification 2.2.1, ignore them if they are not functions. If you want to learn more about microtasks and macro tasks, please check out my article on Event Loop here.

then(onFulfilled, onRejected) {
  // Make them empty functions for now, and change them later
  if (typeofonFulfilled ! = ='function') {
    onFulfilled = (a)= > {};
  }
  if (typeofonRejected ! = ='function') {
    onRejected = (a)= > {};
  }
  if (this.status === 'fulfilled') {
    // Asynchronous execution will be implemented using setTimeout
    setTimeout((a)= > {
      onFulfilled(this.value);
    });
  }
  if (this.status === 'rejected') {
    setTimeout((a)= > {
      onRejected(this.value); }); }}Copy the code

The situation here is that the state of the Promise has changed when the then function is expected to be executed.

If the Promise is an asynchronous function, then the state of the Promise has not changed, then the two functions accepted by the THEN need to be saved until resolve or reject, which are also executed asynchronously.

then(onFulfilled, onRejected) {
  // Temporarily make them synchronous empty functions, which can be changed later
  if (typeofonFulfilled ! = ='function') {
    onFulfilled = (a)= > {};
  }
  if (typeofonRejected ! = ='function') {
    onRejected = (a)= > {};
  }
  // If the state of the Promise has not changed when executing then, store the two functions first
  if (this.status === 'pending') {
    this.callbacks.push({
      onFulfilled: (a)= > {
        setTimeout((a)= > {
          onFulfilled();
        });
      },
      onRejected: (a)= > {
        setTimeout((a)= >{ onRejected(); }); }}); }if (this.status === 'fulfilled') {
    // Asynchronous execution will be implemented using setTimeout
    setTimeout((a)= > {
      / / 2.2.5
      onFulfilled.call(undefined.this.value);
    });
  }
  if (this.status === 'rejected') {
    setTimeout((a)= > {
      / / 2.2.5
      onRejected.call(undefined.this.value);
    });
  }
}
resolve(value) {
  // State protection
  if (this.status ! = ='pending') {
    return;
  }
  // Change the state and assign
  this.status = 'fulfilled';
  this.value = value;
  // If there are values in the array of callback functions, then was executed before, and the function accepted by then needs to be called
  this.callbacks.forEach((callback) = > {
    / / 2.2.5
    callback.onFulfilled.call(undefined, value);
  });
}
reject(reason) {
  // State protection
  if (this.status ! = ='pending') {
    return;
  }
  // Change the state and assign
  this.status = 'rejected';
  this.value = reason;
  // If there are values in the array of callback functions, then was executed before, and the function accepted by then needs to be called
  this.callbacks.forEach((callback) = > {
    / / 2.2.5
    callback.onRejected.call(undefined, reason);
  });
}
Copy the code

In this way, both 2.2.2 and 2.2.3 of the specification are implemented.

Moreover, because the onFulfilled and onRejected in THEN are implemented asynchronously, it also meets specification 2.2.4. It will be called after the Promise code is implemented.

According to specification 2.2.5, there is also no this when onFulfilled and onRejected are called, so use. Call to specify undefined as this.

Specification 2.2.6 directly executes multiple THEN if the state of the Promise has changed before the THEN is executed. Otherwise, the function parameters of then will be stored in the callbacks array, which will be called successively, implementing specification 2.2.6.

4. Chain operation

A Promise has a then method, and then can be then, so let then return a Promise. According to specification 2.2.7, we must also make then return a Promise.

According to specification 2.2.7.1, whether ondepressing and onRejected, the value returned by them will be successfully processed as the value of resolve of the new Promise returned by THEN.

According to 2.2.7.2, if ondepressing and onRejected throw an error E, it will be treated as the new Promise reject value returned by THEN.

then(onFulfilled, onRejected) {
  // then returns a Promise
  return new PROMISE((resolve, reject) = > {
    // Make them empty functions for now, and change them later
    if (typeofonFulfilled ! = ='function') {
      onFulfilled = (a)= > {};
    }
    if (typeofonRejected ! = ='function') {
      onRejected = (a)= > {};
    }
    // If the state of the Promise has not changed when executing then, store the two functions first
    if (this.status === 'pending') {
      this.callbacks.push({
        // There needs to be a change here
        onFulfilled: (a)= > {
            setTimeout((a)= > {
              try {
                / / 2.2.5
                const result = onFulfilled.call(undefined.this.value);
                // The return value of onFulfilled is called as the value of resolve for the new Promise
                resolve(result);
              } catch (error) {
                // If an error is thrown, the new Promise reject value is calledreject(error); }}); },onRejected: (a)= > {
            setTimeout((a)= > {
              try {
                / / 2.2.5
                const result = onRejected.call(undefined.this.value);
                // The return value of onRejected is called as the new Promise's resolve value
                resolve(result);
              } catch (error) {
                // If an error is thrown, the new Promise reject value is calledreject(error); }}); }}); }if (this.status === 'fulfilled') {
      // Asynchronous execution will be implemented using setTimeout
      setTimeout((a)= > {
        try {
          / / 2.2.5
          const result = onFulfilled.call(undefined.this.value);
          // The return value of onFulfilled is called as the value of resolve for the new Promise
          resolve(result);
        } catch (error) {
          // If an error is thrown, the new Promise reject value is calledreject(error); }}); }if (this.status === 'rejected') {
      setTimeout((a)= > {
        try {
          / / 2.2.5
          const result = onRejected.call(undefined.this.value);
          // The return value of onRejected is called as the new Promise's resolve value
          resolve(result);
        } catch (error) {
          // If an error is thrown, the new Promise reject value is calledreject(error); }}); }}); }Copy the code

2.2.7.3 and 2.2.7.4 indicate that if the onFulfilled and onRejected of THEN are not functions, The new Promise will succeed or fail using a Promise’s resolve or reject value, which will inherit the state and value of the previous Promise.

Let me give you an example

new Promise((resolve, reject) = > {
  /** * executes the function body */
})
  .then()
  .then(
    function A() {},
    function B() {});Copy the code

Since the arguments to the first THEN are not functions, pass-through occurs, so the two arguments accepted by the latter THEN, function A and function B, are called based on the state and value of the previous Promise.

The code above is the same as the code below.

new Promise((resolve, reject) = > {
  /** * executes the function body */
})
  .then(
    function A() {},
    function B() {});Copy the code

Ok, let’s implement this specification.

If your last Promise was resolve, then I will use then as resolve, reject, then I will use then as reject.

There’s a little bit of a detour here, but I hope you can look at this very carefully and understand it completely.

if (typeofonFulfilled ! = ='function') {
  onFulfilled = (value) = > {
    // The preceding Promise is resolve, which will be called ondepressing
    // Then the new Promise will also resolve
    // Pass the state and value to the then of the then
    resolve(value);
  };
}
if (typeofonRejected ! = ='function') {
  onRejected = (reason) = > {
    // If the previous Promise is reject, onRejected is called
    // Then the new Promise is reject
    // Pass the state and value to the then of the then
    reject(reason);
  };
}
Copy the code

And actually, we could simplify this to something like this.

if (typeofonFulfilled ! = ='function') {
  onFulfilled = resolve;
}
if (typeofonRejected ! = ='function') {
  onRejected = reject;
}
Copy the code

That completes the chain operation for Promise.

5. Implement 2.3.1

Let’s move on to specification 2.3.

If a Promise and a resolve or Reject call have the same value, then the Promise should be in a Reject state with a TypeError value.

The code is as follows:

constructor(executor) {
  if (typeofexecutor ! = ='function') {
    throw new TypeError(`Promise resolver ${executor} is not a function`);
  }
  // Initial state
  this.status = 'pending';
  / / initial value
  this.value = null;
  // Initial callback array
  this.callbacks = [];
  // Class is in strict mode by default, so you need to bind this
  try {
    executor(this.resolve.bind(this), this.reject.bind(this));
  } catch (error) {
    // Catches TypeError thrown by resolve and reject as reject
    this.reject(error); }}/** ** code */
resolve(value) {
  // State protection
  if (this.status ! = ='pending') {
    return;
  }
  // If the promise and resolve calls have the same value, an error is thrown
  if (value === this) {
    throw new TypeError(a); }// Change the state and assign
  this.status = 'fulfilled';
  this.value = value;
  // If there are values in the array of callback functions, then was executed before, and the function accepted by then needs to be called
  this.callbacks.forEach((callback) = > {
    callback.onFulfilled.call(undefined, value);
  });
}
reject(reason) {
  // State protection
  if (this.status ! = ='pending') {
    return;
  }
  // If the promise and reject calls have the same value, an error is thrown
  if (value === this) {
    throw new TypeError(a); }// Change the state and assign
  this.status = 'rejected';
  this.value = reason;
  this.callbacks.forEach((callback) = > {
    callback.onRejected.call(undefined, reason);
  });
}
Copy the code

6. Implement 2.3.3

Specification 2.3.2 is a subset of the 2.3.3 case, so we can implement 2.3.3 directly.

All specification 2.3.3 says is add the following lines of code to resolve and reject.

if (value instanceof Object) {
  / / 2.3.3.1 2.3.3.2
  const then = value.then;
  / / 2.3.3.3
  if (typeof then === 'function') {
    return then.call(
      value,
      this.resolve.bind(this),
      this.reject.bind(this)); }}Copy the code

As for specification 2.3.3.2, it is not stated that x. Chen is an exception, but that an exception occurs during the value process, and the code is expressed as follows:

// This is not true
const X = {
  then: new Error()}// It is something like this
const x = {};
Object.defineProperty(x, 'then', {
  get: function() {
    throw new Error('y'); }});new Promise((resolve, reject) = > {
  resolve(x)
}).then((value) = > {
  console.log('fulfilled', value)
}, (reason) => {
  console.log('rjected', reason)
})
Copy the code

Since x. teng threw an exception that was caught by a try catch in constructor, reject is eliminated.

Specification 2.3.4 does not require a special implementation; it is normal.

7. Complete implementation

Here is the A+ specification to go again, the implementation of the Promise is as follows:

class PROMISE {
  constructor(executor) {
    if (typeofexecutor ! = ='function') {
      throw new TypeError(`Promise resolver ${executor} is not a function`);
    }
    // Initial state
    this.status = 'pending';
    / / initial value
    this.value = null;
    // Initial callback array
    this.callbacks = [];
    // Class is in strict mode by default, so you need to bind this
    try {
      executor(this.resolve.bind(this), this.reject.bind(this));
    } catch (error) {
      // Catches TypeError thrown by resolve and reject as reject
      this.reject(error);
    }
  }
  then(onFulfilled, onRejected) {
    // then returns a Promise
    return new PROMISE((resolve, reject) = > {
      // Then pass through
      if (typeofonFulfilled ! = ='function') {
        onFulfilled = resolve;
      }
      if (typeofonRejected ! = ='function') {
        onRejected = reject;
      }
      // If the state of the Promise has not changed when executing then, store the two functions first
      if (this.status === 'pending') {
        this.callbacks.push({
          onFulfilled: (a)= > {
            setTimeout((a)= > {
              try {
                const result = onFulfilled.call(undefined.this.value);
                // The return value of onFulfilled is called as the value of resolve for the new Promise
                resolve(result);
              } catch (error) {
                // If an error is thrown, the new Promise reject value is calledreject(error); }}); },onRejected: (a)= > {
            setTimeout((a)= > {
              try {
                const result = onRejected.call(undefined.this.value);
                // The return value of onRejected is called as the new Promise's resolve value
                resolve(result);
              } catch (error) {
                // If an error is thrown, the new Promise reject value is calledreject(error); }}); }}); }if (this.status === 'fulfilled') {
        // Asynchronous execution will be implemented using setTimeout
        setTimeout((a)= > {
          try {
            const result = onFulfilled.call(undefined.this.value);
            // The return value of onFulfilled is called as the value of resolve for the new Promise
            resolve(result);
          } catch (error) {
            // If an error is thrown, the new Promise reject value is calledreject(error); }}); }if (this.status === 'rejected') {
        setTimeout((a)= > {
          try {
            const result = onRejected.call(undefined.this.value);
            // The return value of onRejected is called as the new Promise's resolve value
            resolve(result);
          } catch (error) {
            // If an error is thrown, the new Promise reject value is calledreject(error); }}); }}); } resolve(value) {// State protection
    if (this.status ! = ='pending') {
      return;
    }
    // If the promise and resolve calls have the same value, an error is thrown
    if (value === this) {
      throw new TypeError('Chaining cycle detected for promise');
    }
    if (value instanceof Object) {
      / / 2.3.3.1
      const then = value.then;
      / / 2.3.3.3
      if (typeof then === 'function') {
        return then.call(
          value,
          this.resolve.bind(this),
          this.reject.bind(this)); }}// Change the state and assign
    this.status = 'fulfilled';
    this.value = value;
    // If there are values in the array of callback functions, then was executed before, and the function accepted by then needs to be called
    this.callbacks.forEach((callback) = > {
      callback.onFulfilled.call(undefined, value);
    });
  }
  reject(reason) {
    // State protection
    if (this.status ! = ='pending') {
      return;
    }
    // If the promise and reject calls have the same value, an error is thrown
    if (reason === this) {
      throw new TypeError('Chaining cycle detected for promise');
    }
    if (reason instanceof Object) {
      / / 2.3.3.1
      const then = reason.then;
      / / 2.3.3.3
      if (typeof then === 'function') {
        return then.call(
          reason,
          this.resolve.bind(this),
          this.reject.bind(this)); }}// Change the state and assign
    this.status = 'rejected';
    this.value = reason;
    this.callbacks.forEach((callback) = > {
      callback.onRejected.call(undefined, reason); }); }}Copy the code

I won’t separate out the repetitive code here, just to make it easier to read.

8. Static methods

Static methods like resolve, Reject, All, and Race are not part of the A+ specification, and I’ll implement them here as well.

Resolve and reject are similar in that they accept a value and return a Promise. If the accepted value is a Promise, then the state and value of the Promise are inherited.

static resolve(value) {
  return new PROMISE((resolve, reject) = > {
    if (value instanceof PROMISE) {
      value.then(resolve, reject);
    } else{ resolve(value); }}); }static reject(reason) {
  return new PROMISE((resolve, reject) = > {
    if (reason instanceof PROMISE) {
      reason.then(resolve, reject);
    } else{ reject(reason); }}); }Copy the code

All accepts an array of Promises and returns a Promise.

Define a Results array, then iterate through the Promise array, adding each resolve Promise, just as Results adds a resolve value. If Results has the same length as the Promise array, All promises will return the resolve array.

In addition, as long as one of the promises in the Promise array is rejected, all returns a reject Promise.

static all(promiseArray) {
  return new PROMISE((resolve, reject) = > {
    const results = [];
    promiseArray.forEach((promise) = > {
      promise.then((value) = > {
        results.push(value);
        if (results.length === promiseArray.length) {
          resolve(results);
        }
      }, reject);
    });
  });
}
Copy the code

Race also accepts an array of promises and returns a Promise.

If there is only a Promise resolve or Reject in the Promise array, race returns a Promise that is also resolve or Reject.

static race(promiseArray) {
  return new PROMISE((resolve, reject) = > {
    promiseArray.forEach((promise) = > {
      promise.then(resolve, reject);
    });
  });
}
Copy the code

feeling

In the process of implementing the Promise’s A+ specification from scratch, we combed through the details of the Promise and exposed some things that we hadn’t noticed before. In particular, if X is an object with then methods, x will be wrapped as A Promise. This is also a place that has never been touched before.

I’ve done this in TypeScript here, including the test case I wrote. If you like, welcome star.

If you find something wrong with my implementation process, please feel free to discuss it with me.