The preparatory work

  • Promises/A+ : Promisesaplus.com
  • Full test case: github.com/promises-ap…

start

First, get rid of the test case and implement a Promise we remember

  1. A Promise is a constructor whose input parameter takes a function (fn),

  2. The Promise execution calls fn, passing resolve and reject as input arguments (resolve and Reject are both functions)

  3. After the Promise executes, the instance Promise is returned

  4. A promise has three states: Pending, Fullfilled, and Rejected

  5. A promise exists in the then method, which registers the callback functions onFullfilled, onRejeted, and then returns a Promise instance (nextPromise) for chain invocation

    • Inside the THEN method: When a PROMISE is in a non-pending state, the callback is executed.(The result of the callback execution affects nextPromise, so each registered callback and its corresponding nextPromise need to be saved together)
  6. Promise defaults to pending state

    • Set resolve to fullfilled and save the resolve entry parameter(data), triggering onFullfilled
    • Set reject to Rejected and save the reject parameter(data), triggering onRejeted
  7. Callback (onFullfilled, onRejeted) passes in the saved data

    • Callback returns normally: Trigger nextPromise’s resolve, passing in the return value
    • Callback execution catches an error: The nextPromise reject is fired, passing in the caught error message
  8. If the return value of callback is a Promise instance, ensure that the state of nextPromise is synchronized with the state of the returned Promise

function execCallback(promise) {
  const defers = promise.defers;
  while (defers.length > 0) {
    const defer = defers.shift();
    const nextPromise = defer.promise;
    const callback = promise.state === 'fullfilled' ?  defer.onFullfilled : defer.onRejected;
    let cbRes;
    try {
      cbRes = callback(promise.data);
    } catch (err) {
      reject(nextPromise, err);
      continue;
    }
    if (cbRes instanceof Promise) {
      cbRes.then(data= > {          // If cbRes is also a Promise, ensure that nextPromise is in the same state as cbRes
        resolve(nextPromise, data);
      }, err => {
        reject(nextPromise, err);
      });
    } else{ resolve(nextPromise, cbRes); }}}function resolve(promise, data) {
  promise.data = data;
  promise.state = 'fullfilled';
  execCallback(promise);
}

function reject(promise, err) {
  promise.data = err;
  promise.state = 'rejected';
  execCallback(promise);
}

function Promise(fn) {
  this.state = 'pending';    // pending|fullfilled|rejected
  this.data = undefined;
  this.defers = [];     // Save callback and nextPromise

  const promise = this;
  fn(data= > {
    resolve(promise, data);
  }, err => {
    reject(promise, err);
  });
  return this;
};

Promise.prototype.then = function(onFullfilled, onRejected) {
  const nextPromise = new Promise(function() {});
  let defer = {
    promise: nextPromise,
    onFullfilled,
    onRejected
  };   // The execution of the callback will affect nextPromise, so save it together
  this.defers.push(defer)
  if (this.state ! = ='pending') {
    execCallback(this);    // Non-pending state, trigger callback
  }
  return nextPromise;
}

module.exports =  Promise;
Copy the code

Check out the above code on Github

I verified the above code with several commonly used Promise demos and found no problems

Run the test case and the result is:

171 passing (2m) 672 failing 1) 2.2.1: Both 'onFulfilled' and 'onRejected' are optional arguments. If `onRejected` is not afunction, it must be ignored. applied to a promise fulfilled and then chained off of `onFulfilled` is `undefined`:
     Error: timeout of 200ms exceeded. Ensure the done() callback is being called in this test.
  ......
Copy the code

View all logs

Analyze the log and implement specification 2.2.1 first: handle the case where callback is not a function

2.2.1 Both ondepressing and onRejected are optional arguments: This is a big pity. If onRejected is not a function, it must be ignored. 2.2.1.2. it must be ignored.

This is a pity and onRejected are not functions.

Consider the following case:

new Promise((resolve, reject) = > {
  resolve(123);
})
.then(null.null)
.then(data= > {
  console.log('data: ', data)
}, err => {
  console.log('error: ', err)
})
Copy the code

The code is modified as follows:

(^_^ there is an error in line 8: return should not be used, continue should be used, but the test case is still passed. ¶ ¶ ¶)

See the diff

Implementation of specifications 2.2.2, 2.2.3, 2.2.4 failed part: asynchronous callback

This is a big pity. If onFulfilled is a function. This is a big pity. 2.2.2.2. It must not be called before promise is fulfilled. This is a big pity. 2.2.3.2. It must not be called before promise is rejected Execution Context stack contains only platform code. [3.1].

The specifications sum up: Callback needs to be executed asynchronously

So adding setTimeout to execCallback’s method body solves the problem

See the diff

Implement specifications 2.1.2 and 2.1.3: Constraints on state transitions

This is a big pity. 2.1.2.1. Must not transition to any other state. 2.1.2.2. 2.1.3. When rejected, a promise: 2.1.3.1. Must not transition to any other state. 2.1.3.2.

The status can be switched from Pending to Fullfilled or Rejected only once

The changes are as follows:

See the diff

So far, the requirements of specifications 1, 2.1 and 2.2 have been met

Understand specification 2.3: Callback returns a promise & resolve passes a promise processing

The promise callback can return a promise,

If a promise is returned, the then method must return the same state and data as the promise returned by the callback.

const promise1 = new Promise((resolve, reject) = > {
  resolve(123);
});
const promise2 = new Promise((resolve, reject) = > {
  resolve(456);
});
new Promise((resolve, reject) = > {
  resolve(promise1);
}).then(data= > {
  console.log(data)    // expect "123"
  return promise2;
}).then(data= > {
  console.log(data);   // expect "456"
})
Copy the code

Standard 2.3:

  • This ensures the chained transmission of promise state and data, and implements the secondary invocation of asynchronous code, avoiding callback hell
  • It says as long as the object isthenable, can be treated as a promise

Thenable is simply an object or function that contains the then method:

new Promise((resolve, reject) = > {
  resolve('ok');
}).then(data= > {
  return {         // Thenable is returned
    then(resolve, reject) {
      resolve('aaa'); }}; }).then(data= > {
  console.log(data)   // log: aaa
})
Copy the code

Why the thenable definition?

The main purpose is to make different promise implementations work together

For example, if Joe implements Promise1 and Joe implements repository Promise2, then this is how it works

new Promise1(resolve= > {
  resolve(new Promise2())
})
Copy the code

As for the implementation code, it is not possible to determine Promise by running through the test case, so it is specified that thenable is used to determine whether it can be used collaboratively

Implementation Specification 2.3

The previous test log reported an error pointing to 2.3.1

X | 2.3.1. Promise and refer to the same object, reject promise with a TypeError as the reason.

The case is as follows

let promise = new Promise((resolve, reject) => {
  resolve(123);
});
let nextPromise = promise.then(data => {
  returnnextPromise; // Throw TypeError})Copy the code

Code changes:

See the diff

2.3.3 & 2.3.4

thanable

Note:

  • Both the execCallback and resolve functions handle Thenable
  • Promise can also be treated as thenable

Here are the functions used to handle thenable:

/** * if data is not thenable, return false * @param promise: thanable Will affect promis * @param data: Callback, or the */ value passed to resolve
function doResolveThenable (promise, data) {    
  if (data && /object|function/.test(typeof data)) {
    let then;
    try {
      then = data.then;
      if (typeofthen ! = ='function') return false;    / / not thenanble
      then.call(data, data => { 
        resolve(promise, data)
      }, err => {
        reject(promise, err)
      })
    } catch(err) {
      reject(promise, err)
    }
  } else {
    return false;   / / not thenanble
  }
  return true;
}
Copy the code

Other changes

See the diff

Take a look atlogThere are only 60 failed cases left

The remaining cases are as follows:

In the thenable then method, if onFullfilled, onRejected, and throw are executed successively, the first execution takes effect and the subsequent execution is ignored

let promise = new Promise((resolve, reject) = > {
  resolve('ok');
}).then(data= > {
  return {
    then(onFullfilled, onRejected) { 
      onFullfilled(111);   // Only this sentence can be executed
      onFullfilled(222);
      onRejected(333);
      onRejected(444);
      throw (Awesome!); }}})Copy the code

Code modification:

See the diff

Finally, the test cases are all over, and we’re done


Github.com/mlxiao93/pr…