字節(jié)飛書面試——請實現(xiàn) Promise.all
前言
金三銀四,身為大四即將成為畢業(yè)生的我迫不及待地將簡歷投進了字節(jié)的飛書部門,本想著掂量一下幾斤幾兩,沒想到這一掂就露餡了??,去大廠的夢想就這么跌倒在了Promsie.all上。但年輕人總是要有斗志的,從哪里跌到就從哪里爬起來!下面是復(fù)盤時間。
何為Promise.all?
Promise.all 是 es6 Promise 對象上的一個方法,它的功能就是將多個Promise實例包裝成一個promise實例。以下是 MDN 對 Promise.all 的描述:
Promise.all()
方法接收一個 promise 的 iterable
類型(注:Array,Map,Set都屬于ES6的iterable類型)的輸入,并且只返回一個`Promise`[1]實例, 那個輸入的所有
promise 的 resolve 回調(diào)的結(jié)果是一個數(shù)組。這個`Promise`[2]的 resolve 回調(diào)執(zhí)行是在所有輸入的 promise
的 resolve 回調(diào)都結(jié)束,或者輸入的 iterable 里沒有 promise 了的時候。它的 reject
回調(diào)執(zhí)行是,只要任何一個輸入的 promise 的 reject 回調(diào)執(zhí)行或者輸入不合法的 promise
就會立即拋出錯誤,并且reject的是第一個拋出的錯誤信息。
我戴上我的300度近視眼鏡,仔細地提取出這段描述中的關(guān)鍵字:
Promise.all 的返回值是一個新的 Promise 實例。
Promise.all 接受一個可遍歷的數(shù)據(jù)容器,容器中每個元素都應(yīng)是 Promise 實例。咱就是說,假設(shè)這個容器就是數(shù)組。
數(shù)組中每個
Promise 實例都成功時(由pendding狀態(tài)轉(zhuǎn)化為fulfilled狀態(tài)),Promise.all 才成功。這些 Promise
實例所有的 resolve 結(jié)果回按原來的順序集合在一個數(shù)組中作為 Promise.all 的 resolve 的結(jié)果。
數(shù)組中只要有一個 Promise 實例失?。ㄓ蓀endding狀態(tài)轉(zhuǎn)化為rejected狀態(tài)),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
復(fù)制代碼
注意
上面
p4 和 p5 在未傳入 Promise.all 時需要注釋掉,因為一個調(diào)用了 reject 的 Promise 實例如果沒有使用
.catch() 方法去捕獲錯誤會報錯。但如果 Promise 實例定義了自己的 .catch,就不會觸發(fā) Promise.all 的
.catch() 方法。
OK,理論存在,實踐開始!
手動實現(xiàn)Promise.all
`Promise.all` 接受一個數(shù)組,返回值是一個新的 `Promise` 實例
Promise.MyAll = function (promises) {
return new Promise((resolve, reject) => {
})
}
復(fù)制代碼
數(shù)組中所有
`Promise` 實例都成功,`Promise.all` 才成功。不難想到,咱得需要一個數(shù)組來收集這些 `Promise` 實例的
`resolve` 結(jié)果。但有句俗話說得好:“不怕一萬,就怕萬一”,萬一數(shù)組里面有元素不是 `Promise`咋辦 —— 那就得用
`Promise.resolve()` 把它辦了。這里還有一個問題,`Promise` 實例是不能直接調(diào)用 `resolve` 方法的,咱得在
`.then()` 中去收集結(jié)果。注意要保持結(jié)果的順序。
Promise.MyAll = function (promises) {
let arr = []
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
Promise.resolve(item).then(res => {
arr[i] = res
})
})
})
}
復(fù)制代碼
將收集到的結(jié)果(數(shù)組`arr`)作為參數(shù)傳給外層的 `resolve` 方法。這里咱們肯定是有一個判斷條件的,如何判斷所有 `Promise` 實例都成功了呢?新手容易寫出這句代碼(沒錯就是我本人了??):
if (arr.length === promises.length) resolve(arr)
復(fù)制代碼
咱仔細想想
Promise 使用來干嘛的 —— 處理異步任務(wù)。對呀,異步任務(wù)很多都需要花時間呀,如果這些 Promise 中最后一個先完成呢?那 arr
數(shù)組不就只有最后一項了,前面的所有項都是 empty。所以這里咱們應(yīng)該創(chuàng)建一個計數(shù)器,每有一個 Promise 實例成功,計數(shù)器加一:
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)
})
})
})
}
復(fù)制代碼
最后就是處理失敗的情況了,這里有兩種寫法,第一種是用 `.catch()` 方法捕獲失?。?br>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)
})
})
}
復(fù)制代碼
第二種寫法就是給 .then() 方法傳入第二個參數(shù),這個函數(shù)是處理錯誤的回調(diào)函數(shù):
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)
})
})
}
復(fù)制代碼
測試案例
致此 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
復(fù)制代碼
“OhOhOhOh~~~~”,與原生的
Promise.all運行結(jié)果不能說很像,只能說一模一樣。老話說的好,趁熱打鐵——正在火候上。我打開某個學(xué)習(xí)網(wǎng)站(MDN Web Docs
\(mozilla.org\)[3]),了解到 Promise 對象用于同時處理多個 Promise 的方法還有
Promise.race、Promise.any、Promise.allSettle。從小老師就教會了咱們舉一反三,仔細看了這三個方法的描述之后,我還真給反出來了??。
Promise.race
Promise.race 從字面意思理解就是賽跑,以狀態(tài)變化最快的那個 Promise 實例為準(zhǔn),最快的 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 延時一秒
復(fù)制代碼
理論存在,實踐開始
手寫Promise.race
整體流程與 Promise 差不多,只是對數(shù)組中的 Promise 實例處理的邏輯不一樣,這里我們需要將最快改變狀態(tài)的 Promise 結(jié)果作為 Promise.race 的結(jié)果,相對來說就比較簡單了,代碼如下:
Promise.MyRace = function (promises) {
return new Promise((resolve, reject) => {
// 這里不需要使用索引,只要能循環(huán)出每一項就行
for (const item of promises) {
Promise.resolve(item).then(resolve, reject)
}
})
}
復(fù)制代碼
測試案例
還是剛才幾個案例,咱就不重復(fù)寫了??
// 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 延時一秒
復(fù)制代碼
可以看到,結(jié)果與原生的 Promise.race 是一致的,成功!
Promise.any
Promise.any
與 Promise.all 可以看做是相反的。Promise.any 中只要有一個 Promise 實例成功就成功,只有當(dāng)所有的
Promise 實例失敗時 Promise.any 才失敗,此時Promise.any 會把所有的失敗/錯誤集合在一起,返回一個失敗的
promise和`AggregateError`[4]類型的實例。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
復(fù)制代碼
可以看出,如果 Promise.any 中有多個成功的 Promise 實例,則以最快成功的那個結(jié)果作為自身 resolve 的結(jié)果。
OK,理論存在,實踐開始
手寫Promise.any
依葫蘆畫瓢,咱們先寫出 `Promise.any` 的整體結(jié)構(gòu):
Promise.MyAny = function (promises) {
return new Promise((resolve, reject) => {
promises.forEach((item, i) => {
})
})
}
復(fù)制代碼
這里跟`Promise.all`
的邏輯是反的,咱們需要收集 `reject` 的 `Promise`,也需要一個數(shù)組和計數(shù)器,用計數(shù)器判斷是否所有的 `Promise`
實例都失敗。另外在收集失敗的 `Promise` 結(jié)果時咱需要打上一個失敗的標(biāo)記方便分析結(jié)果。
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成功'))
})
})
})
}
復(fù)制代碼
這里我沒有使用 MDN 上規(guī)定的 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成功
復(fù)制代碼
Promise.allSettled
有時候,咱代碼人總是會有點特殊的需求:如果咱希望一組 Promise 實例無論成功與否,都等它們異步操作結(jié)束了在繼續(xù)執(zhí)行下一步操作,這可如何是好?于是就出現(xiàn)了 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秒' }
// ]
復(fù)制代碼
可以看到,與
Promise.any 類似,Promise.allSettled 也給所有收集到的結(jié)果打上了標(biāo)記。而且 Promise.allSettled
是不會變成 rejected 狀態(tài)的,不管一組 Promise 實例的各自結(jié)果如何,Promise.allSettled 都會轉(zhuǎn)變?yōu)?
fulfilled 狀態(tài)。
OK,理論存在,實踐開始
手寫 Promise.allSettled
咱就是說,得用個數(shù)組把所有的
Promise 實例的結(jié)果(無論成功與否)都收集起來,判斷收集完了(所有 Promise 實例狀態(tài)都改變了),咱就將這個收集到的結(jié)果
resolve 掉。收集成功 Promise 結(jié)果的邏輯咱們在 Promise.all 中實現(xiàn)過,收集失敗 Promise 結(jié)果咱們在
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)
})
})
})
}
復(fù)制代碼
這代碼,邏輯上雖說沒問題,但各位優(yōu)秀的程序員們肯定是看不順眼的,怎么會有兩段重復(fù)的代碼捏,不行,咱得封裝一下。
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')
})
})
})
}
復(fù)制代碼
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秒' }
// ]
復(fù)制代碼
致此,大功告成,我可以驕傲地對媽媽說:“媽媽,我再也不怕 Promise.all”了
作者:滑稽鴨 https://juejin.cn/post/7069805387490263047
作者:滑稽鴨
歡迎關(guān)注微信公眾號 :前端開發(fā)愛好者
添加好友備注【進階學(xué)習(xí)】拉你進技術(shù)交流群