首頁 > 軟體

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

2022-07-05 18:01:41

前言

恰逢 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靜態實現的資料請關注it145.com其它相關文章!


IT145.com E-mail:sddin#qq.com