The main technical points

  1. Chained method calls
  2. Status mechanism Management
  3. Prototype method call
  4. Task queue scheduling

Code implementation

Keep your mouth shut and start writing.

The simplest peomise

To use a Promise, you must new a Promise object and then perform a successful callback

new Promise(function (resolve, reject) {
  resolve(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});
Copy the code

So promise is a constructor and contains a prototype method for THEN.

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';
  
  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;
  
  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
  }
  function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
  }
  
  // 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else {
    reject(this.rejectData)
  }
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});
Copy the code

This implements a simple Promise, but it fails when wrapped with a setTimeout.

Added logic to support setTimeout

Since setTimeout is a macro task, it needs to wait for the completion of the execution to execute the callback of THEN. Therefore, it needs to add an execution queue and invoke THEN after the completion of execution

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';

  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;

  // 6.2 Replenishes the queue field
  this.queues = [];

  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
    // 6.3 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
    // 6.4 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}// 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  const self = this;
  // 6. Add pendding status judgment
  if (this.status === 'pendding') {
    this.queues.push(function () {
      if (self.status === 'resolved') {
        resolve(self.resolveData)
      } else {
        reject(self.rejectData)
      }
    })
  } else if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else {
    reject(this.rejectData)
  }
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 2
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(2)},1000)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});
Copy the code

The code is copied in full, just focus on test case 2 and Step 6. Historical test cases are reserved to ensure that historical code is not affected by modification of PromiseA.

In practice, there are catches as well as then, and exceptions are often handled by catch.

Add catch and perfect logic

In the use of Promise, catch calls the corresponding chain, indicating that catch is a prototype method, focusing on how to skip the then chain to call the chain of catch

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';

  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;

  // 6.2 Replenishes the queue field
  this.queues = [];

  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
    // 6.3 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
    // 6.4 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}// 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  const self = this;
  // 6. Add pendding status judgment
  if (this.status === 'pendding') {
    this.queues.push(function () {
      if (self.status === 'resolved') {
        resolve(self.resolveData)
      } else {
        reject(self.rejectData)
      }
    })
  } else if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else if (this.status === 'rejected' && reject) {
    // 1
    reject(this.rejectData)
  } else {
    // 7.2 Chain calls need to export themselves
    return this; }}// 7.3 Supplement catch prototype method
PromiseA.prototype.catch = function (reject) {
  reject(this.rejectData)
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 2
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(2)},1000)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 3
new PromiseA(function (resolve, reject) {
  reject(3)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});
Copy the code

Copy all the code, just focus on test case 3 and Step 7. Historical test cases are reserved to ensure that historical code is not affected by modification of PromiseA.

Test case 3 executes successfully, exporting this to resolve the chain problem without affecting history.

Improved catch, fixed chain calls

However, an error occurs after setTimeout is added. It is necessary to solve the problem of calling then or catch after setTimeout task

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';

  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;

  // 6.2 Replenishes the queue field
  this.queues = [];

  // 8.4 Replenish the exception queue
  this.rejectedQueues = [];

  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
    // 6.3 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
    // 6.4 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= > fn());
    }
    8.5 Add exception queue logic
    if (self.rejectedQueues.length) {
      self.rejectedQueues.forEach(fn= > fn());
    } else if (self.queues) {
      for (var i = 0; i < self.queues.length; i++) {
        varfunc = self.queues[i].bind(self); func(); }}}// 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  const self = this;
  // 6. Add pendding status judgment
  if (this.status === 'pendding') {
    this.queues.push(function () {
      if (self.status === 'resolved') {
        resolve(self.resolveData)
      } else if (self.status === 'rejected' && reject) {
        // 8.1 Improve queue judgment
        reject(self.rejectData)
      }
    })
    return this;
  } else if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else if (this.status === 'rejected' && reject) {
    // 1
    reject(this.rejectData)
  } else {
    // 7.2 Chain calls need to export themselves
    return this; }}// 7.3 Supplement catch prototype method
PromiseA.prototype.catch = function (reject) {
  const self = this;
  // 8.2 Improve exception judgment
  if (this.status === 'pendding') {
    // 8.3 Replenish the exception queue
    this.rejectedQueues.push(function () {
      if (self.status === 'rejected' && reject) {
        reject(self.rejectData)
      }
    })
  } else if (this.status === 'rejected') {
    reject(this.rejectData)
  }
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 2
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(2)},1000)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 3
new PromiseA(function (resolve, reject) {
  reject(3)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 4
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(4)},1000)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});
Copy the code

The code is copied in full, just focus on test case 4 and Step 8. Historical test cases are reserved to ensure that historical code is not affected by modification of PromiseA.

At this point, only one layer of chain calls is implemented, and the promise requires multiple layers of chain calls

Multilayer chain perfection

Continue to supplement multi-layer chain logic, compatible with a variety of situations

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';

  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;

  // 6.2 Replenishes the queue field
  this.queues = [];

  // 8.4 Replenish the exception queue
  this.rejectedQueues = [];

  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
    // 6.3 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= >fn()); }}function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
    // 6.4 Add queue logic
    if (self.queues.length) {
      self.queues.forEach(fn= > fn());
    }
    8.5 Add exception queue logic
    if (self.rejectedQueues.length) {
      self.rejectedQueues.forEach(fn= >fn()); }}// 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  const self = this;
  // 6. Add pendding status judgment
  if (this.status === 'pendding') {
    this.queues.push(function () {
      if (self.status === 'resolved') {
        // 9.1 resolve will return a new Promise, which is accepted here
        var result = resolve(self.resolveData)
        if (result) {
          Check the execution status of a promise in resolve
          if (result.status === 'resolved') {
            // 9.3 If successful, modify the status data to enter the next chain
            self.status = 'resolved';
            self.resolveData = result.resolveData;
          } else {
            // 9.4 If an exception occurs, check whether there is an exception queue and execute the exception queue method
            self.status = 'rejected';
            self.rejectData = result.rejectData;
            if (self.rejectedQueues.length) {
              self.rejectedQueues.forEach(fn= >fn()); }}}}else if (self.status === 'rejected' && reject) {
        // 8.1 Improve queue judgment
        reject(self.rejectData)
      }
    })
    return this;
  } else if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else if (this.status === 'rejected' && reject) {
    // 1
    reject(this.rejectData)
  } else {
    // 7.2 Chain calls need to export themselves
    return this; }}// 7.3 Supplement catch prototype method
PromiseA.prototype.catch = function (reject) {
  const self = this;
  // 8.2 Improve exception judgment
  if (this.status === 'pendding') {
    // 8.3 Replenish the exception queue
    this.rejectedQueues.push(function () {
      if (self.status === 'rejected' && reject) {
        reject(self.rejectData)
      }
    })
  } else if (this.status === 'rejected') {
    reject(this.rejectData)
  }
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 2
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(2)},1000)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 3
new PromiseA(function (resolve, reject) {
  reject(3)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 4
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(4)},1000)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 5
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    resolve(5)},1000)
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    resolve(6)
  })
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    reject(7)
  })
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});
Copy the code

Copy all the code, just focus on test case 5 and Step 9. Historical test cases are reserved to ensure that historical code is not affected by modification of PromiseA.

Add setTimeout to fix chain macro task failure

When a setTimeout is encountered, the chain calls fail and the output is chaotic. The above example perfected the chain, but since it was a synchronous operation, there was no problem. After adding setTimeout, there was a problem.

The main reason is the instance pointing problem, which requires some logic about this(or instance) pointing.

function PromiseA(callback) {
  // 1. Verify parameters
  if(! callback)throw new Error('callback is required! ');
  var self = this;
  // 2. State Pendding, resolved, Rejected
  this.status = 'pendding';

  // 4.2 Success or Failure Data needs to be stored
  this.resolveData = undefined;
  this.rejectData = undefined;

  // 6.2 Replenishes the queue field
  this.queues = [];

  // 8.4 Replenish the exception queue
  this.rejectedQueues = [];

  // 4.1 Callback requires two parameters, defined here
  function resolve(data) {
    self.resolveData = data;
    self.status = 'resolved';
    // 6.3 Add queue logic
    if (self.queues.length) {
      // 10.1 Add the bind method to change this to point to a specific instance
      for (var i = 0; i < self.queues.length; i++) {
        varfunc = self.queues[i].bind(self); func(); }}}function reject(data) {
    self.rejectData = data;
    self.status = 'rejected';
    8.5 Add exception queue logic
    if (self.rejectedQueues.length) {
      // add the bind method to change this to refer to a specific instance
      for (var i = 0; i < self.rejectedQueues.length; i++) {
        varfunc = self.rejectedQueues[i].bind(self); func(); }}else if (self.queues) {
      for (var i = 0; i < self.queues.length; i++) {
        varfunc = self.queues[i].bind(self); func(); }}}// 3. Execute the promise. Note that the synchronization code may report an error, and you need to actively catch the error
  try {
    callback(resolve, reject)
  } catch(err) {
    reject(err)
  }
}

// 5. Prototype mount then method
PromiseA.prototype.then = function (resolve, reject) {
  // 6. Add pendding status judgment
  if (this.status === 'pendding') {
    this.queues.push(function () {
      // Delete self. This refers to the instance of the current function
      var self = this;
      if (self.status === 'resolved') {
        // 9.1 resolve will return a new Promise, which is accepted here
        var result = resolve(self.resolveData)
        if (result) {
          // 10.4 Resolve After the resolve command is executed, change the state to Pendding to prevent subsequent THEN output
          self.status = 'pendding';
          // 10.5 Inherits the parent callback and deletes the parent callback to prevent further calls
          result.queues = self.queues.slice(1)
          self.queues.splice(1, self.queues.length)
          result.rejectedQueues = self.rejectedQueues
          Check the execution status of a promise in resolve
          if (result.status === 'resolved') {
            // 9.3 If successful, modify the status data to enter the next chain
            self.status = 'resolved';
            self.resolveData = result.resolveData;
          } else if (result.status === 'rejected') {
            // 9.4 If an exception occurs, check whether there is an exception queue and execute the exception queue method
            self.status = 'rejected';
            self.rejectData = result.rejectData;
            if (self.rejectedQueues.length) {
              self.rejectedQueues.forEach(fn= >fn.bind(result)()); }}}}else if (self.status === 'rejected' && reject) {
        // 8.1 Improve queue judgment
        reject(self.rejectData)
      }
      10.6 Execute the next Promise queue
      if (result && result.queues) {
        for (var i = 0; i < result.queues.length; i++) {
          varfunc = result.queues[i].bind(self); func(); }}})return this;
  } else if (this.status === 'resolved') {
    resolve(this.resolveData)
  } else if (this.status === 'rejected' && reject) {
    // 1
    reject(this.rejectData)
  } else {
    // 7.2 Chain calls need to export themselves
    return this; }}// 7.3 Supplement catch prototype method
PromiseA.prototype.catch = function (reject) {
  // 8.2 Improve exception judgment
  if (this.status === 'pendding') {
    // 8.3 Replenish the exception queue
    this.rejectedQueues.push(function () {
      // 10.7 Delete self from the original definition, the current function this refers to the instance, to prevent confusion with the outer definition of self
      var self = this;
      if (self.status === 'rejected' && reject) {
        reject(self.rejectData)
      }
    })
  } else if (this.status === 'rejected') {
    reject(this.rejectData)
  }
}

// Test case 1
new PromiseA(function (resolve, reject) {
  reject(1)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 2
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(2)},1000)
}).then(res= > {
  console.log('success' + res);
}, err= > {
  console.log('failure' + err);
});

// Test case 3
new PromiseA(function (resolve, reject) {
  reject(3)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 4
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    reject(4)},1000)
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 5
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    resolve(5)},1000)
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    resolve(6)
  })
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    reject(7)
  })
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});

// Test case 6
new PromiseA(function (resolve, reject) {
  setTimeout(function (){
    resolve(8)},1000)
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    setTimeout(function (){
      resolve(9)},1000)
  })
}).then(res= > {
  console.log('success' + res);
  return new PromiseA((resolve, reject) = > {
    reject(10)
  })
}).then(res= > {
  console.log('success' + res);
}).catch(err= > {
  console.log('failure' + err);
});
Copy the code

The code is copied in full, just focus on test case 6 and Step 10. Historical test cases are reserved to ensure that historical code is not affected by modification of PromiseA.

conclusion

The last piece of code above is the JS implementation of Promise, and the test coverage is not very comprehensive, so there may be other problems.

Implementing Promise with JS is primarily about deepening the technical aspects of native JS such as microtasks, queues, this Pointers, constructors, prototypes, and so on.

Those who are interested can try it for themselves