Lesson 42-Promise Source Code Analysis

Core Principles of Promise

Promise State Management Mechanism

The core of a Promise lies in the management of its three immutable states:

  1. Pending: Initial state, neither fulfilled nor rejected.
  2. Fulfilled: Operation completed successfully.
  3. Rejected: Operation failed.

State Transition Rules:

  • Pending → Fulfilled (can only transition once).
  • Pending → Rejected (can only transition once).
  • Fulfilled/Rejected states are immutable.
// Simplified state management implementation
class MyPromise {
  constructor(executor) {
    this.state = 'pending'; // Initial state
    this.value = undefined; // Value on success
    this.reason = undefined; // Reason for failure
    this.onFulfilledCallbacks = []; // Success callback queue
    this.onRejectedCallbacks = []; // Failure callback queue

    const resolve = (value) => {
      if (this.state === 'pending') {
        this.state = 'fulfilled';
        this.value = value;
        // Execute all success callbacks
        this.onFulfilledCallbacks.forEach(fn => fn());
      }
    };

    const reject = (reason) => {
      if (this.state === 'pending') {
        this.state = 'rejected';
        this.reason = reason;
        // Execute all failure callbacks
        this.onRejectedCallbacks.forEach(fn => fn());
      }
    };

    try {
      executor(resolve, reject); // Execute executor immediately
    } catch (err) {
      reject(err); // Reject if executor throws an error
    }
  }
}

Implementation of then Method for Chaining

The then method is central to Promise, enabling chaining and asynchronous execution:

class MyPromise {
  // ... Previous state management code

  then(onFulfilled, onRejected) {
    // Handle value passthrough
    onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
    onRejected = typeof onRejected === 'function' ? onRejected : err => { throw err; };

    const promise2 = new MyPromise((resolve, reject) => {
      if (this.state === 'fulfilled') {
        // Execute onFulfilled asynchronously
        setTimeout(() => {
          try {
            const x = onFulfilled(this.value);
            resolvePromise(promise2, x, resolve, reject); // Handle return value
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.state === 'rejected') {
        // Execute onRejected asynchronously
        setTimeout(() => {
          try {
            const x = onRejected(this.reason);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      } else if (this.state === 'pending') {
        // If state is undetermined, store callbacks in queue
        this.onFulfilledCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onFulfilled(this.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });

        this.onRejectedCallbacks.push(() => {
          setTimeout(() => {
            try {
              const x = onRejected(this.reason);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }, 0);
        });
      }
    });

    return promise2; // Return new Promise for chaining
  }
}

// Handle thenable objects and Promise return values
function resolvePromise(promise2, x, resolve, reject) {
  // Prevent circular references
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // Prevent multiple calls
  let called = false;

  if (x !== null && (typeof x === 'object' || typeof x === 'function')) {
    try {
      const then = x.then;
      if (typeof then === 'function') {
        // If x is a Promise or thenable object
        then.call(
          x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise2, y, resolve, reject); // Recursively resolve
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          }
        );
      } else {
        // Ordinary object/value
        resolve(x);
      }
    } catch (e) {
      if (called) return;
      called = true;
      reject(e);
    }
  } else {
    // Primitive value
    resolve(x);
  }
}

Asynchronous Execution and Microtask Queue

Promise callback execution follows the Microtask mechanism:

  1. Difference Between Microtasks and Macrotasks:
    • Microtasks: Promise callbacks, MutationObserver, process.nextTick (Node.js).
    • Macrotasks: setTimeout, setInterval, I/O operations, UI rendering.
  2. Implementing a Microtask Queue:
// Simple microtask queue implementation
const microTaskQueue = [];
let isFlushing = false;

function flushMicroTasks() {
  if (isFlushing) return;
  isFlushing = true;

  while (microTaskQueue.length) {
    const task = microTaskQueue.shift();
    try {
      task();
    } catch (e) {
      console.error('Microtask error:', e);
    }
  }

  isFlushing = false;
}

// Simulate Promise asynchronous execution
function asyncExecute(fn) {
  microTaskQueue.push(fn);
  // In browsers, a more efficient microtask API is used
  // Here, setTimeout is used for simulation (actually a macrotask)
  setTimeout(flushMicroTasks, 0);
}

// Usage in then method
if (this.state === 'fulfilled') {
  asyncExecute(() => {
    try {
      const x = onFulfilled(this.value);
      resolvePromise(promise2, x, resolve, reject);
    } catch (e) {
      reject(e);
    }
  });
}

Microtask APIs in Actual Browsers:

  • Modern browsers use microtask queues for Promise callbacks.
  • Node.js uses process.nextTick and microtask queues.
  • The queueMicrotask API allows direct microtask usage.

Promise.resolve and Promise.reject

These static methods provide shortcuts for creating resolved/rejected Promises:

class MyPromise {
  // ... Previous code

  static resolve(value) {
    // If value is already a Promise, return it
    if (value instanceof MyPromise) {
      return value;
    }

    // Otherwise, create a new resolved Promise
    return new MyPromise(resolve => {
      resolve(value);
    });
  }

  static reject(reason) {
    // Create a new rejected Promise
    return new MyPromise((_, reject) => {
      reject(reason);
    });
  }

  // Handle thenable objects
  static resolve(value) {
    if (value && typeof value === 'object' && typeof value.then === 'function') {
      return new MyPromise((resolve, reject) => {
        value.then(resolve, reject);
      });
    }
    return new MyPromise(resolve => resolve(value));
  }
}

Promise Error Handling Mechanism

Promises provide robust error handling:

  1. Executor Error Handling:
new Promise((resolve, reject) => {
  throw new Error('executor error'); // Automatically caught and rejected
}).catch(err => console.log(err)); // Catch error
  1. Error Handling in then Method:
new Promise((resolve) => {
  resolve(1);
})
.then(
  value => { throw new Error('then error') }, // Error in success callback
  err => console.log('This won’t run') // Won’t catch the above error
)
.catch(err => console.log(err)); // Catches all errors
  1. Error Propagation Mechanism:
new Promise((resolve) => {
  resolve(1);
})
.then(value => {
  console.log(value);
  return 2;
})
.then(value => {
  throw new Error('chain error');
})
.then(value => {
  console.log('This won’t run');
})
.catch(err => console.log(err)); // Catches any error in the chain
  1. Implementation of finally Method:
class MyPromise {
  // ... Previous code

  finally(callback) {
    return this.then(
      value => MyPromise.resolve(callback()).then(() => value),
      reason => MyPromise.resolve(callback()).then(() => { throw reason; })
    );
  }
}

Membership Required

You must be a member to access this content.

View Membership Levels

Already a member? Log in here
Share your love