字節飛書面試promise.all實現示例

前言

金三銀四,身為大四即將成為畢業生的我迫不及待地將簡歷投進瞭字節的飛書部門,本想著掂量一下幾斤幾兩,沒想到這一掂就露餡瞭🤣,去大廠的夢想就這麼跌倒在瞭Promsie.all上。但年輕人總是要有鬥志的,從哪裡跌到就從哪裡爬起來!下面是復盤時間。

何為Promise.all?

Promise.all 是 es6 Promise 對象上的一個方法,它的功能就是將多個Promise實例包裝成一個promise實例。以下是 MDN 對 Promise.all 的描述:

Promise.all() 方法接收一個 promise 的 iterable 類型(註:Array,Map,Set都屬於ES6的iterable類型)的輸入,並且隻返回一個Promise實例, 那個輸入的所有 promise 的 resolve 回調的結果是一個數組。這個Promise的 resolve 回調執行是在所有輸入的 promise 的 resolve 回調都結束,或者輸入的 iterable 裡沒有 promise 瞭的時候。它的 reject 回調執行是,隻要任何一個輸入的 promise 的 reject 回調執行或者輸入不合法的 promise 就會立即拋出錯誤,並且reject的是第一個拋出的錯誤信息。

我戴上我的300度近視眼鏡,仔細地提取出這段描述中的關鍵字:

  • Promise.all 的返回值是一個新的 Promise 實例。
  • Promise.all 接受一個可遍歷的數據容器,容器中每個元素都應是 Promise 實例。咱就是說,假設這個容器就是數組。
  • 數組中每個 Promise 實例都成功時(由pendding狀態轉化為fulfilled狀態),Promise.all 才成功。這些 Promise 實例所有的 resolve 結果會按照原來的順序集合在一個數組中作為 Promise.all 的 resolve 的結果。
  • 數組中隻要有一個 Promise 實例失敗(由pendding狀態轉化為rejected狀態),Promise.all 就失敗。Promise.all 的 .catch() 會捕獲到這個 reject。

原生 Promise.all 測試

咱先看看原生的Promise.all的是啥效果。

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延時一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延時兩秒')
  }, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延時1.5秒')
  }, 1500)
})
// 所有Promise實例都成功
Promise.all([p1, p2, p3])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 2秒後打印 [ 'p1', 'p2 延時一秒', 'p3 延時兩秒' ]
// 一個Promise實例失敗
Promise.all([p1, p2, p4])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected
// 一個延時失敗的Promise
 Promise.all([p1, p2, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // 1.5秒後打印 p5 rejected
// 兩個Promise實例失敗
Promise.all([p1, p4, p5])
  .then(res => {
    console.log(res)
  })
  .catch(err => console.log(err)) // p4 rejected

註意

上面 p4 和 p5 在未傳入 Promise.all 時需要註釋掉,因為一個調用瞭 reject 的 Promise 實例如果沒有使用 .catch() 方法去捕獲錯誤會報錯。但如果 Promise 實例定義瞭自己的 .catch,就不會觸發 Promise.all 的 .catch() 方法。

OK,理論存在,實踐開始!

手動實現Promise.all

Promise.all 接受一個數組,返回值是一個新的 Promise 實例

Promise.MyAll = function (promises) {
  return new Promise((resolve, reject) => {
  })
}

數組中所有 Promise 實例都成功,Promise.all 才成功。不難想到,咱得需要一個數組來收集這些 Promise 實例的 resolve 結果。但有句俗話說得好:“不怕一萬,就怕萬一”,萬一數組裡面有元素不是 Promise咋辦 —— 那就得用 Promise.resolve() 把它辦瞭。這裡還有一個問題,Promise 實例是不能直接調用 resolve 方法的,咱得在 .then() 中去收集結果。註意要保持結果的順序。

Promise.MyAll = function (promises) {
  let arr = []
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
      })
    }) 
  })
}

將收集到的結果(數組arr)作為參數傳給外層的 resolve 方法。這裡咱們肯定是有一個判斷條件的,如何判斷所有 Promise 實例都成功瞭呢?新手容易寫出這句代碼(沒錯就是我本人瞭😭):

if (arr.length === promises.length) resolve(arr)

咱仔細想想 Promise 使用來幹嘛的 —— 處理異步任務。對呀,異步任務很多都需要花時間呀,如果這些 Promise 中最後一個先完成呢?那 arr 數組不就隻有最後一項瞭,前面的所有項都是 empty。所以這裡咱們應該創建一個計數器,每有一個 Promise 實例成功,計數器加一:

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      })
    })
  })
}

最後就是處理失敗的情況瞭,這裡有兩種寫法,第一種是用 .catch() 方法捕獲失敗:

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      }).catch(reject)
    })
  })
}

第二種寫法就是給 .then() 方法傳入第二個參數,這個函數是處理錯誤的回調函數:

Promise.MyAll = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = res
        count += 1
        if (count === promises.length) resolve(arr)
      }, reject)
    })
  })
}

測試案例

致此 Promise.all 大功告成,趕緊拿來測試一下(摩拳擦掌):

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延時一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延時兩秒')
  }, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延時1.5秒')
  }, 1500)
})
// 所有 Promsie 都成功
Promise.MyAll([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 2秒後打印 [ 'p1', 'p2 延時一秒', 'p3 延時兩秒' ]
// 一個 Promise 失敗
Promise.MyAll([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
// 一個延時失敗的 Promise
Promise.MyAll([p1, p2, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1.5秒後打印 p5 rejected 延時1.5秒
// 兩個失敗的 Promise
Promise.MyAll([p1, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected

“OhOhOhOh~~~~”,與原生的 Promise.all運行結果不能說很像,隻能說一模一樣。老話說的好,趁熱打鐵——正在火候上。我打開某個學習網站(MDN Web Docs (mozilla.org)),瞭解到 Promise 對象用於同時處理多個 Promise 的方法還有 Promise.race、Promise.any、Promise.allSettle。從小老師就教會瞭咱們舉一反三,仔細看瞭這三個方法的描述之後,我還真給反出來瞭😄。

Promise.race

Promise.race 從字面意思理解就是賽跑,以狀態變化最快的那個 Promise 實例為準,最快的 Promise 成功 Promise.race 就成功,最快的 Promise 失敗 Promise.race 就失敗。

咱來看看原生 Promise.race 效果

原生 Promise.race 測試

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延時一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延時兩秒')
  }, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延時1秒')
  }, 1500)
})
// p1無延時,p2延時1s,p3延時2s
Promise.race([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// p4無延時reject
Promise.race([p4, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
// p5 延時1.5秒reject,p2延時1s
Promise.race([p5, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1s後打印: p2 延時一秒

理論存在,實踐開始

手寫Promise.race

整體流程與 Promise 差不多,隻是對數組中的 Promise 實例處理的邏輯不一樣,這裡我們需要將最快改變狀態的 Promise 結果作為 Promise.race 的結果,相對來說就比較簡單瞭,代碼如下:

Promise.MyRace = function (promises) {
  return new Promise((resolve, reject) => {
    // 這裡不需要使用索引,隻要能循環出每一項就行
    for (const item of promises) {
      Promise.resolve(item).then(resolve, reject)
    }
  })
}

測試案例

還是剛才幾個案例,咱就不重復寫瞭😁

// p1無延時,p2延時1s,p3延時2s
Promise.MyRace([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// p4無延時reject
Promise.MyRace([p4, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p4 rejected
// p5 延時1.5秒reject,p2延時1s
Promise.MyRace([p5, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 1s後打印: p2 延時一秒

可以看到,結果與原生的 Promise.race 是一致的,成功!

Promise.any

Promise.any 與 Promise.all 可以看做是相反的。Promise.any 中隻要有一個 Promise 實例成功就成功,隻有當所有的 Promise 實例失敗時 Promise.any 才失敗,此時Promise.any 會把所有的失敗/錯誤集合在一起,返回一個失敗的 promise 和AggregateError類型的實例。MDN 上說這個方法還處於試驗階段,如果 node 或者瀏覽器版本過低可能無法使用,各位看官自行測試下。

原生 Promise.any 測試

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延時一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延時兩秒')
  }, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延時1.5秒')
  }, 1500)
})
// 所有 Promise 都成功
Promise.any([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// 兩個 Promise 成功
Promise.any([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// 隻有一個延時成功的 Promise
Promise.any([p2, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p2 延時1秒
// 所有 Promise 都失敗
Promise.any([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // AggregateError: All promises were rejected

可以看出,如果 Promise.any 中有多個成功的 Promise 實例,則以最快成功的那個結果作為自身 resolve 的結果。

OK,理論存在,實踐開始

手寫Promise.any

依葫蘆畫瓢,咱們先寫出 Promise.any 的整體結構:

Promise.MyAny = function (promises) {
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
    })
  })
}

這裡跟Promise.all 的邏輯是反的,咱們需要收集 reject 的 Promise,也需要一個數組和計數器,用計數器判斷是否所有的 Promise 實例都失敗。另外在收集失敗的 Promise 結果時咱需要打上一個失敗的標記方便分析結果。

Promise.MyAny = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(resolve, err => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) reject(new Error('沒有promise成功'))
      })
    })
  })
}

這裡我沒有使用 MDN 上規定的 AggregateError 實例,手寫嘛,隨心所欲一點,寫自己看著舒服的😄

測試案例

// 所有 Promise 都成功
Promise.MyAny([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// 兩個 Promise 成功
Promise.MyAny([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p1
// 隻有一個延時成功的 Promise
Promise.MyAny([p2, p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // p2 延時1秒
// 所有 Promise 都失敗
Promise.MyAny([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err)) // 沒有promise成功

Promise.allSettled

有時候,咱代碼人總是會有點特殊的需求:如果咱希望一組 Promise 實例無論成功與否,都等它們異步操作結束瞭在繼續執行下一步操作,這可如何是好?於是就出現瞭 Promise.allSettled。

原生 Promise.allSettled 測試

const p1 = Promise.resolve('p1')
const p2 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p2 延時一秒')
  }, 1000)
})
const p3 = new Promise((resolve, reject) => {
  setTimeout(() => {
    resolve('p3 延時兩秒')
  }, 2000)
})
const p4 = Promise.reject('p4 rejected')
const p5 = new Promise((resolve, reject) => {
  setTimeout(() => {
    reject('p5 rejected 延時1.5秒')
  }, 1500)
})
// 所有 Promise 實例都成功
Promise.allSettled([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) 
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延時一秒' },
//   { status: 'fulfilled', value: 'p3 延時兩秒' }
// ]
// 有一個 Promise 失敗
Promise.allSettled([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延時一秒' },
//   { status: 'rejected' , value: 'p4 rejected' }
// ]
// 所有 Promise 都失敗
Promise.allSettled([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'rejected', reason: 'p4 rejected' },
//   { status: 'rejected', reason: 'p5 rejected 延時1.5秒' }
// ]

可以看到,與 Promise.any 類似,Promise.allSettled 也給所有收集到的結果打上瞭標記。而且 Promise.allSettled 是不會變成 rejected 狀態的,不管一組 Promise 實例的各自結果如何,Promise.allSettled 都會轉變為 fulfilled 狀態。

OK,理論存在,實踐開始

手寫 Promise.allSettled

咱就是說,得用個數組把所有的 Promise 實例的結果(無論成功與否)都收集起來,判斷收集完瞭(所有 Promise 實例狀態都改變瞭),咱就將這個收集到的結果 resolve 掉。收集成功 Promise 結果的邏輯咱們在 Promise.all 中實現過,收集失敗 Promise 結果咱們在 Promise.any 中處理過。這波,這波是依葫蘆畫瓢——照樣。

Promise.MyAllSettled = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        arr[i] = { status: 'fulfilled', val: res }
        count += 1
        if (count === promises.length) resolve(arr)
      }, (err) => {
        arr[i] = { status: 'rejected', val: err }
        count += 1
        if (count === promises.length) resolve(arr)
      })
    })
  })
}

這代碼,邏輯上雖說沒問題,但各位優秀的程序員們肯定是看不順眼的,怎麼會有兩段重復的代碼捏,不行,咱得封裝一下。

Promise.MyAllSettled = function (promises) {
  let arr = [],
    count = 0
  return new Promise((resolve, reject) => {
    const processResult = (res, index, status) => {
      arr[index] = { status: status, val: res }
      count += 1
      if (count === promises.length) resolve(arr)
    }
    promises.forEach((item, i) => {
      Promise.resolve(item).then(res => {
        processResult(res, i, 'fulfilled')
      }, err => {
        processResult(err, i, 'rejected')
      })
    })
  })
}

perfect,俗話說得好:沒病走兩步。老樣子,給代碼跑幾個案例。

測試案例

// 所有 Promise 實例都成功
Promise.MyAllSettled([p1, p2, p3])
  .then(res => console.log(res))
  .catch(err => console.log(err)) 
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延時一秒' },
//   { status: 'fulfilled', value: 'p3 延時兩秒' }
// ]
// 有一個 MyAllSettled 失敗
Promise.allSettled([p1, p2, p4])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'fulfilled', value: 'p1' },
//   { status: 'fulfilled', value: 'p2 延時一秒' },
//   { status: 'rejected' , value: 'p4 rejected' }
// ]
// 所有 MyAllSettled 都失敗
Promise.allSettled([p4, p5])
  .then(res => console.log(res))
  .catch(err => console.log(err))
// [
//   { status: 'rejected', reason: 'p4 rejected' },
//   { status: 'rejected', reason: 'p5 rejected 延時1.5秒' }
// ]

致此,大功告成,我可以驕傲地對媽媽說:“媽媽,我再也不怕 Promise.all”瞭

結語

這次字節飛書面試對我來說是一個巨大的機遇,第一次體驗面大廠的感覺,可能有暴躁老哥要說瞭:“字節面試題就這?你是水文章騙贊的吧”。害,沒辦法,主要是我太菜瞭,從代碼不知為何物到現在前端學習者,爾來8月右一周矣,水平確實比較次,面試官比較和善,就沒有為難我,問的問題都比較基礎。但我仍然收獲頗豐,感謝字節團隊,感謝前端這個包容、進步的環境,我會好好總結這次面試,盡可能地提升自己,加油!

參考文章

因為實現不瞭Promise.all,一場面試涼涼瞭 

Promise 對象 – ECMAScript 6入門 (ruanyifeng.com)

更多關於字節面試promise.all的資料請關註WalkonNet其它相關文章!

推薦閱讀: