手寫 Promise:實現符合 Promises-A - 規範的 Promise

本文將指導你一步步實現一個符合 Promises/A + 規範的 Promise,幫助你深入理解 Promise 的工作原理和實現細節。

一. 整體介紹

Promise 是 JavaScript 中非常重要的一個概念,它是一種用於處理異步操作的編程模型。Promise 提供了一種優雅的方式來處理異步操作的成功或失敗,以及它們之間的依賴關係。這使得我們可以避免回調地獄(Callback Hell)的問題,編寫更清晰、更易於理解的代碼。

Promises/A + 是一個對 Promise 行爲的開放規範,它規定了 Promise 應該如何表現和實現。遵循這個規範的 Promise 實現可以確保它們之間的互操作性,使得我們可以在不同的庫和框架中輕鬆地使用它們。在本文中,我們將實現一個名爲HYPromise的類,它將遵循 Promises/A + 規範。

二. 實現目標

我們將實現以下功能和方法,使HYPromise符合 Promises/A + 規範:

  1. HYPromise 構造函數及基本狀態

  2. resolve 和 reject 方法

  3. then 方法

  4. catch 方法

  5. finally 方法

  6. HYPromise.resolve 和 HYPromise.reject 靜態方法

  7. HYPromise.all 和 HYPromise.race 靜態方法

  8. 實現 Promise.allSettled 和 Promise.any 靜態方法

三. 實現過程

第 1 步:定義 HYPromise 構造函數,並實現基本的狀態屬性

首先,我們需要創建一個名爲HYPromise的類,並定義三種狀態:pending、fulfilled 和 rejected。HYPromise 類的構造函數將接收一個執行器(executor)函數作爲參數。執行器函數會立即執行,並接收兩個參數:resolvereject,它們分別用於將 Promise 狀態從 pending 更改爲 fulfilled 或 rejected。

我們還需要在類中實現狀態的改變以及狀態改變時對應的值(value)或原因(reason)的存儲。

下面是 HYPromise 類的基本結構以及構造函數的實現:

class HYPromise {
  constructor(executor) {
    // 初始化狀態爲pending
    this.status = 'pending';
    // 初始化成功的值爲undefined
    this.value = undefined;
    // 初始化失敗的原因爲undefined
    this.reason = undefined;

    // 定義resolve方法
    const resolve = (value) ={
      // 只有在pending狀態才能更改狀態和值
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
      }
    };

    // 定義reject方法
    const reject = (reason) ={
      // 只有在pending狀態才能更改狀態和原因
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
      }
    };

    // 立即執行執行器函數
    try {
      executor(resolve, reject);
    } catch (error) {
      // 如果執行器函數拋出異常,將Promise狀態更改爲rejected
      reject(error);
    }
  }
}

在這一步中,我們定義了 HYPromise 類的基本結構,並實現了構造函數以及狀態屬性的初始化。我們還定義了resolvereject方法,並在執行器函數中使用它們。如果執行器函數拋出異常,我們會捕獲它並將 Promise 狀態更改爲 rejected。

第 2 步:實現 resolve 和 reject 兩個核心方法

接下來,我們需要在 HYPromise 類中實現兩個核心方法:resolvereject。這兩個方法用於處理異步操作的結果。我們會將這兩個方法從構造函數中提取出來,並作爲類的實例方法。同時,我們需要處理異步操作的結果,將成功或失敗的處理函數存儲在隊列中,在resolvereject方法中逐個執行這些處理函數。

下面是 HYPromise 類的實現:

class HYPromise {
  constructor(executor) {
    // 初始化狀態爲pending
    this.status = 'pending';
    // 初始化成功的值爲undefined
    this.value = undefined;
    // 初始化失敗的原因爲undefined
    this.reason = undefined;
    // 初始化成功處理函數隊列
    this.onFulfilledCallbacks = [];
    // 初始化失敗處理函數隊列
    this.onRejectedCallbacks = [];

    // 定義resolve方法
    const resolve = (value) ={
      // 只有在pending狀態才能更改狀態和值
      if (this.status === 'pending') {
        this.status = 'fulfilled';
        this.value = value;
        // 執行所有成功處理函數
        this.onFulfilledCallbacks.forEach(callback => callback());
      }
    };

    // 定義reject方法
    const reject = (reason) ={
      // 只有在pending狀態才能更改狀態和原因
      if (this.status === 'pending') {
        this.status = 'rejected';
        this.reason = reason;
        // 執行所有失敗處理函數
        this.onRejectedCallbacks.forEach(callback => callback());
      }
    };

    // 立即執行執行器函數
    try {
      executor(resolve, reject);
    } catch (error) {
      // 如果執行器函數拋出異常,將Promise狀態更改爲rejected
      reject(error);
    }
  }
}

在這一步中,我們實現了resolvereject方法。當 Promise 狀態從 pending 變爲 fulfilled 或 rejected 時,我們將執行相應的處理函數隊列中的函數。此外,我們還對構造函數中的異常處理進行了優化。

第 3 步:實現 then 方法

接下來,我們需要在 HYPromise 類中實現then方法。then方法用於爲 Promise 實例註冊成功和失敗的處理函數。它返回一個新的 Promise 實例,以便我們可以鏈式調用。

爲了符合 Promises/A + 規範,我們需要實現一個名爲resolvePromise的輔助函數。這個函數用於處理then方法返回的新 Promise 實例以及它們的成功和失敗處理函數的結果。

首先,我們實現resolvePromise輔助函數:

function resolvePromise(promise2, x, resolve, reject) {
  // 1. 如果 promise2 和 x 相同,拋出 TypeError
  if (promise2 === x) {
    return reject(new TypeError('Chaining cycle detected for promise'));
  }

  // 標記是否已調用,防止多次調用
  let called = false;

  // 2. 如果 x 是 HYPromise 實例
  if (x instanceof HYPromise) {
    // 根據 x 的狀態調用 resolve 或 reject
    x.then(
      y ={
        resolvePromise(promise2, y, resolve, reject);
      },
      reason ={
        reject(reason);
      }
    );
  } else if (x !== null && (typeof x === 'object' || typeof x === 'function')) { // 3. 如果 x 是對象或函數
    try {
      // 獲取 x 的 then 方法
      const then = x.then;
      if (typeof then === 'function') { // 如果 then 是函數
        // 使用 x 作爲上下文調用 then 方法
        then.call(
          x,
          y ={ // 成功回調
            if (called) return; // 如果已經調用過,直接返回
            called = true;
            // 遞歸處理 y
            resolvePromise(promise2, y, resolve, reject);
          },
          reason ={ // 失敗回調
            if (called) return; // 如果已經調用過,直接返回
            called = true;
            reject(reason);
          }
        );
      } else { // 如果 then 不是函數
        // 直接調用 resolve
        resolve(x);
      }
    } catch (error) { // 如果獲取或調用 then 方法拋出異常
      if (called) return; // 如果已經調用過,直接返回
      called = true;
      reject(error);
    }
  } else { // 4. 如果 x 不是對象或函數
    // 直接調用 resolve
    resolve(x);
  }
}

接下來,我們在 HYPromise 類中實現then方法:

then(onFulfilled, onRejected) {
  // 如果不傳處理函數,則使用默認處理函數
  onFulfilled = typeof onFulfilled === 'function' ? onFulfilled : value => value;
  onRejected = typeof onRejected === 'function' ? onRejected : reason ={ throw reason };

  // 創建一個新的Promise實例,稱爲promise2
  const promise2 = new HYPromise((resolve, reject) ={
    if (this.status === 'fulfilled') {
      // 使用setTimeout保證異步調用
      setTimeout(() ={
        try {
          // 調用onFulfilled,並獲取返回值
          const x = onFulfilled(this.value);
          // 使用返回值x和新的Promise實例promise2來處理resolve和reject
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          // 如果處理函數拋出異常,則將promise2狀態更改爲rejected
          reject(error);
        }
      });
    } else if (this.status === 'rejected') {
      // 使用setTimeout保證異步調用
      setTimeout(() ={
        try {
          // 調用onRejected,並獲取返回值
          const x = onRejected(this.reason);
          // 使用返回值x和新的Promise實例promise2來處理resolve和reject
          resolvePromise(promise2, x, resolve, reject);
        } catch (error) {
          // 如果處理函數拋出異常,則將promise2狀態更改爲rejected
          reject(error);
        }
      });
    } else if (this.status === 'pending') {
      // 如果當前Promise狀態仍爲pending,將處理函數加入相應的隊列中
      this.onFulfilledCallbacks.push(() ={
        // 使用setTimeout保證異步調用
        setTimeout(() ={
          try {
            // 調用onFulfilled,並獲取返回值
            const x = onFulfilled(this.value);
            // 使用返回值x和新的Promise實例promise2來處理resolve和reject
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            // 如果處理函數拋出異常,則將promise2狀態更改爲rejected
            reject(error);
          }
        });
      });

      this.onRejectedCallbacks.push(() ={
        // 使用setTimeout保證異步調用
        setTimeout(() ={
          try {
            // 調用onRejected,並獲取返回值
            const x = onRejected(this.reason);
            // 使用返回值x和新的Promise實例promise2來處理resolve和reject
            resolvePromise(promise2, x, resolve, reject);
          } catch (error) {
            // 如果處理函數拋出異常,則將promise2狀態更改爲rejected
            reject(error);
          }
        });
      });
    }
  });

  // 返回新的Promise實例,以便鏈式調用
  return promise2;
}

第 4 步:實現 catch 方法

catch 方法是一個語法糖,它等價於調用 then 方法時僅傳入一個失敗處理函數。我們需要在 HYPromise 類中實現這個方法。

下面是 HYPromise 類的 catch 方法實現:

class HYPromise {
  // ...其他代碼

  catch(onRejected) {
    // 調用then方法,僅傳入失敗處理函數
    return this.then(null, onRejected);
  }
}

在這一步中,我們實現了catch方法。它只接受一個參數:onRejected,這個參數是失敗的處理函數。我們通過調用then方法並傳入null作爲成功處理函數來實現這個方法。

第 5 步:實現 finally 方法

finally 方法也是一個語法糖,它用於在 Promise 實例上註冊一個處理函數,無論 Promise 是成功還是失敗,該處理函數都會被調用。我們需要在 HYPromise 類中實現這個方法。

下面是 HYPromise 類的 finally 方法實現:

class HYPromise {
  // ...其他代碼

  finally(callback) {
    // 調用then方法,傳入兩個相同的處理函數
    return this.then(
      value ={
        // 創建一個新的Promise實例,確保異步執行callback
        return HYPromise.resolve(callback()).then(() => value);
      },
      reason ={
        // 創建一個新的Promise實例,確保異步執行callback
        return HYPromise.resolve(callback()).then(() ={ throw reason; });
      }
    );
  }
}

在這一步中,我們實現了finally方法。它接受一個參數:callback,這個參數是一個處理函數。無論 Promise 實例成功還是失敗,這個處理函數都會被調用。我們通過調用then方法並傳入兩個相同的處理函數來實現這個方法。這兩個處理函數分別用於成功和失敗的情況,它們都會返回一個新的 Promise 實例,以確保callback是異步執行的。

第 6 步:實現 Promise.resolve 和 Promise.reject 靜態方法

接下來,我們需要在 HYPromise 類中實現兩個靜態方法:resolvereject。這兩個方法可以快速地創建一個已經解決或拒絕的 Promise 實例。

下面是 HYPromise 類的 resolve 和 reject 靜態方法實現:

class HYPromise {
  // ...其他代碼

  static resolve(value) {
    if (value instanceof HYPromise) {
      return value;
    }
    return new HYPromise((resolve, reject) ={
      resolve(value);
    });
  }

  static reject(reason) {
    return new HYPromise((resolve, reject) ={
      reject(reason);
    });
  }
}

在這一步中,我們實現了resolvereject靜態方法。resolve方法接受一個參數:value,用於創建一個已經解決的 Promise 實例。reject方法接受一個參數:reason,用於創建一個已經拒絕的 Promise 實例。

第 7 步:實現 Promise.all 和 Promise.race 靜態方法

最後,我們需要在 HYPromise 類中實現兩個靜態方法:allraceall方法用於將多個 Promise 實例包裝成一個新的 Promise 實例,只有當所有的 Promise 實例都成功時,新的 Promise 實例纔會成功;race方法則是將多個 Promise 實例包裝成一個新的 Promise 實例,只要其中一個 Promise 實例成功或失敗,新的 Promise 實例就會立即成功或失敗。

下面是 HYPromise 類的 all 和 race 靜態方法實現:

class HYPromise {
  // ...其他代碼

  static all(promises) {
    return new HYPromise((resolve, reject) ={
      const result = [];
      let resolvedCount = 0;

      promises.forEach((promise, index) ={
        HYPromise.resolve(promise).then(
          value ={
            result[index] = value;
            resolvedCount++;
            if (resolvedCount === promises.length) {
              resolve(result);
            }
          },
          reason ={
            reject(reason);
          }
        );
      });
    });
  }

  static race(promises) {
    return new HYPromise((resolve, reject) ={
      promises.forEach(promise ={
        HYPromise.resolve(promise).then(
          value ={
            resolve(value);
          },
          reason ={
            reject(reason);
          }
        );
      });
    });
  }
}

在這一步中,我們實現了allrace靜態方法。all方法接受一個數組參數,該數組包含多個 Promise 實例。我們遍歷這個數組,使用HYPromise.resolve將每個實例包裝成一個標準的 Promise 實例。當所有實例都解決時,我們將結果數組傳遞給新的 Promise 實例的resolve方法。race方法的實現類似,我們遍歷輸入數組,當任何一個實例解決或拒絕時,立即調用新的 Promise 實例的resolvereject方法。

第 8 步:實現 Promise.allSettled 和 Promise.any 靜態方法

接下來,我們需要在 HYPromise 類中實現兩個額外的靜態方法:allSettledanyallSettled方法用於將多個 Promise 實例包裝成一個新的 Promise 實例,只要所有的 Promise 實例都完成(成功或失敗),新的 Promise 實例就會成功;any方法則是將多個 Promise 實例包裝成一個新的 Promise 實例,只要其中一個 Promise 實例成功,新的 Promise 實例就會立即成功。如果所有實例都失敗,新的 Promise 實例將失敗。

下面是 HYPromise 類的 allSettled 和 any 靜態方法實現:

class HYPromise {
  // ...其他代碼

  static allSettled(promises) {
    return new HYPromise((resolve, reject) ={
      const result = [];
      let settledCount = 0;

      promises.forEach((promise, index) ={
        HYPromise.resolve(promise).then(
          value ={
            result[index] = { status: 'fulfilled', value };
            settledCount++;
            if (settledCount === promises.length) {
              resolve(result);
            }
          },
          reason ={
            result[index] = { status: 'rejected', reason };
            settledCount++;
            if (settledCount === promises.length) {
              resolve(result);
            }
          }
        );
      });
    });
  }

  static any(promises) {
    return new HYPromise((resolve, reject) ={
      const errors = [];
      let rejectedCount = 0;

      promises.forEach((promise, index) ={
        HYPromise.resolve(promise).then(
          value ={
            resolve(value);
          },
          reason ={
            errors[index] = reason;
            rejectedCount++;
            if (rejectedCount === promises.length) {
              reject(new AggregateError(errors, 'All promises were rejected'));
            }
          }
        );
      });
    });
  }
}

在這一步中,我們實現了allSettledany靜態方法。allSettled方法接受一個數組參數,該數組包含多個 Promise 實例。我們遍歷這個數組,使用HYPromise.resolve將每個實例包裝成一個標準的 Promise 實例。當所有實例都完成(成功或失敗)時,我們將結果數組傳遞給新的 Promise 實例的resolve方法。any方法的實現類似,我們遍歷輸入數組,當任何一個實例解決時,立即調用新的 Promise 實例的resolve方法。當所有實例都拒絕時,我們將錯誤數組傳遞給新的 Promise 實例的reject方法。

四. Promises/A+ 測試

在實現完我們的 HYPromise 之後,我們需要通過 Promises/A + 的測試來驗證我們的實現是否正確。Promises/A + 提供了一組測試用例,我們可以用這些測試用例來確保我們的 HYPromise 滿足 Promises/A + 規範。

第 1 步:安裝 Promises/A + 測試庫

首先,我們需要安裝 Promises/A + 的測試庫。在項目目錄下運行以下命令:

npm init
npm install --save-dev promises-aplus-tests

這將在項目中安裝promises-aplus-tests庫。

第 2 步:編寫測試適配器

接下來,我們需要編寫一個適配器文件,以便promises-aplus-tests庫能夠測試我們的 HYPromise 實現。在項目目錄下創建一個名爲adapter.js的文件,然後在其中添加以下代碼:

const HYPromise = require('./HYPromise'); // 導入我們實現的HYPromise模塊

// 暴露適配器對象
module.exports = {
  resolved: HYPromise.resolve,
  rejected: HYPromise.reject,
  deferred() {
    const result = {};
    result.promise = new HYPromise((resolve, reject) ={
      result.resolve = resolve;
      result.reject = reject;
    });
    return result;
  }
};

這個適配器文件導出了一個對象,其中包含了resolvedrejecteddeferred方法。這些方法分別對應 HYPromise 的resolvereject方法和一個返回延遲對象(包含一個新的 Promise 實例以及對應的 resolve 和 reject 方法)的函數。

第 3 步:運行測試

在項目目錄下創建一個名爲test.js的文件,然後在其中添加以下代碼:

const promisesAplusTests = require('promises-aplus-tests');
const adapter = require('./adapter');

promisesAplusTests(adapter, function (err) {
  if (err) {
    console.error('Promises/A+ 測試失敗:');
    console.error(err);
  } else {
    console.log('Promises/A+ 測試通過');
  }
});

這個文件導入了promises-aplus-tests庫和我們編寫的適配器。然後,我們調用promisesAplusTests函數,傳入適配器對象和一個回調函數。如果測試通過,我們會在控制檯輸出 “Promises/A+ 測試通過”,否則會輸出錯誤信息。

最後,運行以下命令執行測試:

node test.js

如果我們的 HYPromise 實現正確,我們應該看到 “Promises/A+ 測試通過” 的輸出。如果測試失敗,我們需要根據錯誤信息修改我們的 HYPromise 實現,然後重新運行測試,直到所有測試都通過。

至此,我們已經成功地對我們的 HYPromise 實現進行了 Promises/A + 測試。

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/L2dl0woiRl6ghyKoo-sWNA