Promise靜態四兄弟實現示例詳解

前言

恰逢 Promise 也有四個很像的靜態三兄弟(Promise.allPromise.allSettledPromise.racePromise.any),它們接受的參數類型相同,但各自邏輯處理不同,它們具體會有什麼區別那?別急,下面等小包慢慢道來。

在文章的開始,小包先給大傢提出幾個問題:

  • Promise.allPromise.allSettled 有啥區別啊?
  • Promise.race 的運行機制? Promise.any 吶,兩者有啥區別?
  • 四兄弟隻能接受數組作為參數嗎?
  • 四兄弟方法我們應該如何優雅完美的實現?

Promise.all

Promise.all 在目前手寫題中熱度頻度應該是 top5 級別的,所以我們要深刻掌握 Promise.all 方法。下面首先來簡單回顧一下 all 方法。

基礎學習

Promise.all 方法類似於一群兄弟們並肩前行,參數可以類比為一群兄弟,隻有當兄弟全部快樂,all 老大才會收獲快樂;隻要有一個兄弟不快樂,老大就不會快樂。

Promise.all() 方法用於將多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.all([p1, p2, p3]);

Promise.all 方法接受一個數組做參數,p1、p2、p3 都是 Promise 實例。如果不是 Promise 實例,則會先調用 Promise.resolve 方法將參數先轉化為 Promise 實例,之後進行下一步處理。

返回值 p 的狀態由 p1、p2、p3 決定,可以分成兩種情況:

  • 隻有 p1、p2、p3 的狀態都變成 fulfilledp 的狀態才會變成 fulfilled ,此時 p1、p2、p3 的返回值組成一個數組,傳遞給 p 的回調函數。
  • 隻要 p1、p2、p3 之中有一個被 rejectedp 的狀態就變成 rejected ,此時第一個被 reject 的實例的返回值,會傳遞給 p 的回調函數。
// 模擬異步的promise
const p1 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve(1);
  }, 1000);
});
// 普通promise
const p2 = Promise.resolve(2);
// 常數值
const p3 = 3;
// 失敗的promise
const p4 = Promise.reject("error");
// 異步失敗的promise
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject("TypeError");
  }, 1000);
});
// 1. promise全部成功
Promise.all([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失敗的promise
Promise.all([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多個失敗的promise
Promise.all([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error

從上面案例的輸出中,我們可以得出下列結論:

  • p 狀態由參數執行結果決定,全部成功則返回成功,存有一個失敗則失敗
  • 參數為非 Promise 實例,會通過 Promise.resolve 轉化成 Promise 實例
  • 成功後返回一個數組,數組內數據按照參數順序排列
  • 短路效應: 隻會返回第一個失敗信息

Iterator 接口參數

《ES6 入門教程》還指出: Promise.all 方法可以不是數組,但必須具有 Iterator 接口,且返回的每個成員都是 Promise 實例

說實話,加粗部分小包是沒能完全理解的,難道 Promise.all 使用 Iterator 類型時,要求迭代項都是 Promise 實例嗎?我們以 String 類型為例,看 Promise.all 是否可以支持迭代項為非 Promise 實例。

//  ['x', 'i', 'a', 'o', 'b', 'a', 'o']
Promise.all("xiaobao").then((data) => console.log(data));

可見 PromiseIterator 類型的處理與數組相同,如果參數不是 Promise 實例,會先調用 Promise.all 轉化為 Promise 實例。

思路分析

  • Promise.all 會返回一個新 Promise 對象
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {});
};
  • (亮點) all 方法參數可以是數組,同樣也可以是 Iterator 類型,因此應該使用 for of 循環進行遍歷。
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    for (let p of promises) {
    }
  });
};
  • 某些參數有可能未必是 Promise 類型,因此參數使用前先通過 Promise.resolve 轉換
Promise.all = function (promises) {
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      // 保證所有的參數為 promise 實例,然後執行後續操作
      Promise.resolve(p).then((data) => {
        //...
      });
    }
  });
};

Iterator 類型我們是無法得知迭代深度,因此我們要維護一個 count 用來記錄 promise 總數,同時維護 fulfilledCount 代表完成的 promise 數,當 count === fulfilledCount ,代表所有傳入的 Promise 執行成功,返回數據。

Promise.all = function (promises) {
  let count = 0; // promise總數
  let fulfilledCount = 0; // 完成的promise數
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      count++; // promise總數 + 1
      Promise.resolve(p).then((data) => {
        fulfilledCount++; // 完成的promise數量+1
        if (count === fulfilledCount) {
          // 代表最後一個promise完成瞭
          resolve();
        }
      });
    }
  });
};

有可能有的讀者會好奇,為啥 count === fulfilledCount 可以判斷所有的 promise 都完成瞭吶?

Promise.then 方法是 microTasks(微任務),當同步任務執行完畢後,Event Loop 才會去執行 microTaskscount++ 位於同步代碼部分,因此在執行 promise.then 方法之前,已經成功的計算出 promise 的總數。

然後依次執行 promise.then 方法,fulfilledCount 增加,當 count === fulfilledCount 說明所有的 promise 都已經成功完成瞭。

返回數據的順序應該是 all 方法中比較難處理的部分。

  • 創建一個數組 result 存儲所有 promise 成功的數據
  • for of 循環中,使用 let 變量定義 i,其值等於當前的遍歷索引
  • let 定義的變量不會發生變量提升,因此我們直接令 result[i]promise 成功數據,這樣就可以實現按參數輸入順序輸出結果
Promise.all = function (promises) {
  const result = []; // 存儲promise成功數據
  let count = 0;
  let fulfilledCount = 0;
  return new Promise((resolve, reject) => {
    for (let p of promises) {
      // i為遍歷的第幾個promise
      // 使用let避免形成閉包問題
      let i = count;
      count++;
      // 保證所有的參數為 promise 實例,然後執行後續操作
      Promise.resolve(p).then((data) => {
        fulfilledCount++;
        // 將第i個promise成功數據賦值給對應位置
        result[i] = data;
        if (count === fulfilledCount) {
          // 代表最後一個promise完成瞭
          // 返回result數組
          resolve(result);
        }
      });
    }
  });
};

處理一下邊界情況

  • 某個 promise 失敗——直接調用 reject 即可
  • 傳入 promise 數量為 0 ——返回空數組(規范規定)
  • 代碼執行過程拋出異常 —— 返回錯誤信息
// 多餘代碼省略
Promise.all = function (promises) {
    return new Promise((resolve, reject) => {
        // 3.捕獲代碼執行中的異常
        try{
            for (let p of promises) {
                Promise.resolve(p).then(data => {}
                                .catch(reject);  // 1.直接調用reject函數返回失敗原因
                })
            }
            // 2.傳入promise數量為0
            if (count === 0) {
                resolve(result)
            }
        } catch(error) {
            reject(error)
        }
    })
}

源碼實現

我們把上面的代碼匯總一下,加上詳細的註釋,同時測試一下手寫 Promise.all 是否成功。

Promise.all = function (promises) {
  const result = []; // 存儲promise成功數據
  let count = 0; // promise總數
  let fulfilledCount = 0; //完成promise數量
  return new Promise((resolve, reject) => {
    // 捕獲代碼執行中的異常
    try {
      for (let p of promises) {
        // i為遍歷的第幾個promise
        // 使用let避免形成閉包問題
        let i = count;
        count++; // promise總數 + 1
        Promise.resolve(p)
          .then((data) => {
            fulfilledCount++; // 完成的promise數量+1
            // 將第i個promise成功數據賦值給對應位置
            result[i] = data;
            if (count === fulfilledCount) {
              // 代表最後一個promise完成瞭
              // 返回result數組
              resolve(result);
            }
          })
          .catch(reject);
        // 傳入promise數量為0
        if (count === 0) {
          resolve(result); // 返回空數組
        }
      }
    } catch (error) {
      reject(error);
    }
  });
};

測試代碼(使用案例中的測試代碼,附加 Iterator 類型 Stirng):

// 1. promise全部成功
Promise.all([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失敗的promise
Promise.all([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多個失敗的promise
Promise.all([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 4. String 類型
Promise.all("zcxiaobao").then((data) => console.log(data));
// ['z', 'c', 'x', 'i', 'a', 'o', 'b', 'a', 'o']

Promise.allSettled

基礎學習

不是每群兄弟們都會碰到好老大(all 方法),allSettled 方法他並不管兄弟們的死活,他隻管兄弟們是否做瞭,而他的任務就是把所有兄弟的結果返回。

Promise.allSettled() 方法接受一個數組作為參數,數組的每個成員都是一個 Promise 對象,並返回一個新的 Promise 對象。隻有等到參數數組的所有 Promise 對象都發生狀態變更(不管是 fulfilled 還是 rejected),返回的 Promise 對象才會發生狀態變更。

還是以上面的例子為例,我們來看一下與 Promise.all 方法有啥不同。

// 1. promise 全部成功
Promise.allSettled([p1, p2, p3])
  .then((data) => console.log(data)) // [1, 2, 3]
  .catch((error) => console.log(error));
// 2. 存在失敗的 promise
Promise.allSettled([p1, p2, p3, p4])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 3. 存在多個失敗的 promise
Promise.allSettled([p1, p2, p3, p4, p5])
  .then((data) => console.log(data))
  .catch((error) => console.log(error)); // error
// 4. 傳入 String 類型
Promise.allSettled("zc").then((data) => console.log(data));

從輸出結果我們可以發現:

  • allSettled 方法隻會成功,不會失敗
  • 返回結果每個成員為對象,對象的格式固定
    • 如果 promise 成功,對象屬性值 status: fulfilledvalue 記錄成功值
    • 如果 promise 失敗,對象屬性值 status: rejectedreason 記錄失敗原因。
  • allSettled 方法也可以接受 Iterator 類型參數

思路分析

allSettled 方法與 all 方法最大的區別在於兩點:

  • allSettled 方法沒有失敗情況
  • allSettled 方法返回有固定格式

我們可以圍繞這兩點改造 all 方法。

all 方法我們是通過計算成功數量來判斷是否終結,allSettled 方法不計較成功失敗,因此我們需要計算成功/失敗總數量即可。

在累加完成總數量的過程中,分情況構造 allSettled 所需要的數據格式: 成功時壓入成功格式,失敗時壓入失敗格式。

源碼實現

由於有瞭 all 方法手寫的基礎,上面就不一步一步囉嗦的實現瞭。

Promise.allSettled = function (promises) {
  const result = [];
  let count = 0;
  let totalCount = 0; //完成promise數量
  return new Promise((resolve, reject) => {
    try {
      for (let p of promises) {
        let i = count;
        count++; // promise總數 + 1
        Promise.resolve(p)
          .then((res) => {
            totalCount++;
            // 成功時返回成功格式數據
            result[i] = {
              status: "fulfilled",
              value: res,
            };
            // 執行完成
            if (count === totalCount) {
              resolve(result);
            }
          })
          .catch((error) => {
            totalCount++;
            // 失敗時返回失敗格式數據
            result[i] = {
              status: "rejected",
              reason: error,
            };
            // 執行完成
            if (count === totalCount) {
              resolve(result);
            }
          });
        if (count === 0) {
          resolve(result);
        }
      }
    } catch (error) {
      reject(error);
    }
  });
};

Promise.race

基礎學習

race 方法形象化來講就是賽跑機制,隻認第一名,不管是成功的第一還是失敗的第一。

Promise.race() 方法同樣是接收多個 Promise 實例,包裝成一個新的 Promise 實例。

const p = Promise.race([p1, p2, p3]);

上面案例中,隻要 p1、p2、p3 之中有一個實例率先改變狀態,p 的狀態就跟著改變。那個率先改變的 Promise 實例的返回值,就傳遞給 p 的回調函數。

const p1 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        resolve(1)
    },1000)
})
const p2 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        reject(2)
    },2000)
})
const p3 = 3;
// 成功在先,失敗在後
Promise.race([p1, p2]).then(res => {console.log(res)}) // 1
// 同步在先,異步在後
Promise.race([p1, p3]).then(res => console.log(res)) // 3
// String
Promise.race('zc').then(res => console.log(res)) // z

思路分析

race 方法就沒有那麼多彎彎繞繞瞭,隻要某個 promise 改變狀態就返回其對應結果。

因此我們隻需監聽每個 promisethencatch 方法,當發生狀態改變,直接調用 resolvereject 方法即可。

源碼實現

Promise.race(promises) {
    return new Promise((resolve, reject) => {
        for (let p of promises) {
            // Promise.resolve將p進行轉化,防止傳入非Promise實例
            // race執行機制為那個實例發生狀態改變,則返回其對應結果
            // 因此監聽
            Promise.resolve(p).then(resolve).catch(reject);
        }
    })
}

Promise.any

基礎學習

any 方法形象化來說是天選唯一,隻要第一個成功者。如果全部失敗瞭,就返回失敗情況。

ES2021 引入瞭 Promise.any() 方法。該方法接受一組 Promise 實例作為參數,包裝成一個新的 Promise 實例返回。

any 方法與 race 方法很像,也存在短路特性,隻要有一個實例變成 fulfilled 狀態,就會返回成功的結果;如果全部失敗,則返回失敗情況。

// 成功的promise
const p1 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        resolve(1)
    },1000)
})
// 失敗的promise
const p2 = new Promise((resolve, reject) => {
    setTimeout(()=> {
        reject(2)
    },2000)
})
//失敗的promise
const p3 = new Promise((resolve, reject) => {
    reject(3)
})
// 存在一個成功的promise
Promise.any([p1,p2]).then(res => console.log(res))// 1
// 全部失敗的promise
Promise.any([p2,p3]).then(res => console.log(res))
                    .catch(error => console.log(error)) // AggregateError: All promises were rejected
// String類型
Promise.any('zc').then(res => console.log(res)) // z

通過上述輸出結果我們可以發現:

  • any 方法也可以接受 Iterator 格式參數
  • 當一個 promise 實例轉變為 fulfilled 時,any 返回成功的 promise ,值為最早成功的 promise值。
  • promise 全部失敗時,any 返回失敗的 promise ,值固定為 AggregateError: All promises were rejected

思路分析

上面我們分析瞭 any 方法的機制:

  • 某個實例轉化為 fulfilledany 隨之返回成功的 promise。因此這裡我們就可以類似使用 race 的方法,監測每個 promise 的成功。
  • 全部實例轉化為 rejectedany 返回 AggregateError: All promises were rejected。這裡我們可以參考 all 方法的全部成功,才返回成功,因此我們需要累計失敗數量,當 rejectCount === count 時,返回失敗值。

源碼實現

Promise.any = function(promises) {
    return new Promise((resolve,reject) => {
        let count = 0;
        let rejectCount = 0;
        let errors = [];
        let i = 0;
        for (let p of promises) {
            i = count;
            count ++;
            Promise.resolve(p).then(res => {
                resolve(res)
            }).catch(error => {
                errors[i] = error;
                rejectCount ++;
                if (rejectCount === count) {
                    return reject(new AggregateError(errors))
                }
            })
        }
        if(count === 0) return reject(new AggregateError('All promises were rejected'))        
    })
}

以上就是Promise靜態四兄弟實現示例詳解的詳細內容,更多關於Promise靜態實現的資料請關註WalkonNet其它相關文章!

推薦閱讀: