JavaScript 數組的高階函數使用異步操作
吾輩的博客原文: https://blog.rxliuli.com/p/5e...
場景
吾輩是一隻在飛向太陽的螢火蟲
JavaScript 中的數組是一個相當泛用性的數據結構,能當數組,元組,隊列,棧進行操作,更好的是 JavaScript 提供了很多原生的高階函數,便於我們對數組整體操作。
然而,JavaScript 中的高階函數仍有缺陷 -- 異步!當你把它們放在一起使用時,就會感覺到這種問題的所在。
例如現在,有一組 id,我們要根據 id 獲取到遠端服務器 id 對應的值,然後將之打印出來。那麼,我們要怎麼做呢?
const wait = ms => new Promise(resolve => setTimeout(resolve, ms))
async function get(id) {
// 這裏只是爲了模擬每個請求的時間可能是不定的
await wait(Math.random() * id * 100)
return '內容: ' + id.toString()
}
const ids = [1, 2, 3, 4]
你或許會下意識地寫出下面的代碼
ids.forEach(async id => console.log(await get(id)))
事實上,控制檯輸出是無序的,而並非想象中的 1, 2, 3, 4 依次輸出
內容: 2
內容: 3
內容: 1
內容: 4
這是爲什麼呢?原因便是 JavaScript 中數組的高階函數並不會等待異步函數的返回!當你在網絡上搜索時,會發現很多人會說可以使用 for-of
, for-in
解決這個問題。
;(async () => {
for (let id of ids) {
console.log(await get(id))
}
})()
或者,使用 Promise.all
也是一種解決方案
;(async () => {
;(await Promise.all(ids.map(get))).forEach(v => console.log(v))
})()
然而,第一種方式相當於丟棄了 Array 的所有高階函數,再次重返遠古 for
循環時代了。第二種則一定會執行所有的異步函數,即便你需要使用的是 find/findIndex/some/every
這些高階函數。那麼,有沒有更好的解決方案呢?
思考
既然原生的 Array 不支持完善的異步操作,那麼,爲什麼不由我們來實現一個呢?
實現思路:
- 創建異步數組類型
AsyncArray
- 內置一個數組保存當前異步操作數組的值
- 實現數組的高階函數並實現支持異步函數順序執行
- 獲取到內置的數組
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
this._task = []
}
async forEach(fn) {
const arr = this._arr
for (let i = 0, len = arr.length; i < len; i++) {
await fn(arr[i], i, this)
}
}
}
new AsyncArray(...ids).forEach(async id => console.log(await get(id)))
打印結果確實有順序了,看似一切很美好?
然而,當我們再實現一個 map
試一下
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
}
async forEach(fn) {
const arr = this._arr
for (let i = 0, len = arr.length; i < len; i++) {
await fn(arr[i], i, this)
}
}
async map(fn) {
const arr = this._arr
const res = []
for (let i = 0, len = arr.length; i < len; i++) {
res.push(await fn(arr[i], i, this))
}
return this
}
}
調用一下
new AsyncArray(...ids).map(get).forEach(async res => console.log(res))
// 拋出錯誤
// (intermediate value).map(...).forEach is not a function
然而會有問題,實際上 map
返回的是 Promise
,所以我們還必須使用 await
進行等待
;(async () => {
;(await new AsyncArray(...ids).map(get)).forEach(async res =>
console.log(res),
)
})()
是不是感覺超級蠢?吾輩也是這樣認爲的!
鏈式調用加延遲執行
我們可以嘗試使用鏈式調用加延遲執行修改這個 AsyncArray
/**
* 保存高階函數傳入的異步操作
*/
class Action {
constructor(type, args) {
/**
* @field 異步操作的類型
* @type {string}
*/
this.type = type
/**
* @field 異步操作的參數數組
* @type {Function}
*/
this.args = args
}
}
/**
* 所有的操作類型
*/
Action.Type = {
forEach: 'forEach',
map: 'map',
filter: 'filter',
}
/**
* 真正實現的異步數組
*/
class InnerAsyncArray {
constructor(arr) {
this._arr = arr
}
async forEach(fn) {
const arr = this._arr
for (let i = 0, len = arr.length; i < len; i++) {
await fn(arr[i], i, this)
}
this._arr = []
}
async map(fn) {
const arr = this._arr
const res = []
for (let i = 0, len = arr.length; i < len; i++) {
res.push(await fn(arr[i], i, this))
}
this._arr = res
return this
}
async filter(fn) {
const arr = this._arr
const res = []
for (let i = 0, len = arr.length; i < len; i++) {
if (await fn(arr[i], i, this)) {
res.push(arr[i])
}
}
this._arr = res
return this
}
}
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
/**
* @field 保存異步任務
* @type {Action[]}
*/
this._task = []
}
forEach(fn) {
this._task.push(new Action(Action.Type.forEach, [fn]))
return this
}
map(fn) {
this._task.push(new Action(Action.Type.map, [fn]))
return this
}
filter(fn) {
this._task.push(new Action(Action.Type.filter, [fn]))
return this
}
/**
* 終結整個鏈式操作並返回結果
*/
async value() {
const arr = new InnerAsyncArray(this._arr)
let result
for (let task of this._task) {
result = await arr[task.type](...task.args)
}
return result
}
}
使用一下
new AsyncArray(...ids)
.filter(async i => i % 2 === 0)
.map(get)
.forEach(async res => console.log(res))
.value()
可以看到,確實符合預期了,然而每次都要調用 value()
,終歸有些麻煩。
使用 then 以支持 await 自動結束
這裏使用 then()
替代它以使得可以使用 await
自動計算結果
class AsyncArray {
// 上面的其他內容...
/**
* 終結整個鏈式操作並返回結果
*/
async then(resolve) {
const arr = new InnerAsyncArray(this._arr)
let result
for (let task of this._task) {
result = await arr[task.type](...task.args)
}
// 這裏使用 resolve(result) 是爲了兼容 await 的調用方式
resolve(result)
return result
}
}
現在,可以使用 await
結束這次鏈式調用了
await new AsyncArray(...ids).map(get).forEach(async res => console.log(res))
突然之間,我們發現了一個問題,爲什麼會這麼慢?一個個去進行異步操作太慢了,難道就不能一次性全部發送出去,然後有序的處理結果就好了嘛?
併發異步操作
我們可以使用 Promise.all
併發執行異步操作,然後對它們的結果進行有序地處理。
/**
* 併發實現的異步數組
*/
class InnerAsyncArrayParallel {
constructor(arr) {
this._arr = arr
}
async _all(fn) {
return Promise.all(this._arr.map(fn))
}
async forEach(fn) {
await this._all(fn)
this._arr = []
}
async map(fn) {
this._arr = await this._all(fn)
return this
}
async filter(fn) {
const arr = await this._all(fn)
this._arr = this._arr.filter((v, i) => arr[i])
return this
}
}
然後修改 AsyncArray
,使用 _AsyncArrayParallel
即可
class AsyncArray {
// 上面的其他內容...
/**
* 終結整個鏈式操作並返回結果
*/
async then(resolve) {
const arr = new InnerAsyncArrayParallel(this._arr)
let result = this._arr
for (let task of this._task) {
result = await arr[task.type](...task.args)
}
// 這裏使用 resolve(result) 是爲了兼容 await 的調用方式
if (resolve) {
resolve(result)
}
return result
}
}
調用方式不變。當然,由於使用 Promise.all
實現,也同樣受到它的限制 -- 異步操作實際上全部執行了。
串行/並行相互轉換
現在我們的 _AsyncArray
和 _AsyncArrayParallel
兩個類只能二選一,所以,我們需要添加兩個函數用於互相轉換。
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
/**
* @field 保存異步任務
* @type {AsyncArrayAction[]}
*/
this._task = []
/**
* 是否並行化
*/
this._parallel = false
}
// 其他內容...
parallel() {
this._parallel = true
return this
}
serial() {
this._parallel = false
return this
}
async then() {
const arr = this._parallel
? new InnerAsyncArrayParallel(this._arr)
: new InnerAsyncArray(this._arr)
let result = this._arr
for (let task of this._task) {
result = await arr[task.type](...task.args)
}
if (resolve) {
resolve(result)
}
return result
}
}
現在,我們可以在真正執行之前在任意位置對其進行轉換了
await new AsyncArray(...ids)
.parallel()
.filter(async i => i % 2 === 0)
.map(get)
.forEach(async res => console.log(res))
併發執行多個異步操作
然而,上面的代碼有一些隱藏的問題
-
await
之後返回值不是一個數組;(async () => { const asyncArray = new AsyncArray(...ids) console.log(await asyncArray.map(i => i * 2)) // InnerAsyncArray { _arr: [ 2, 4, 6, 8 ] } })()
-
上面的
map
,filter
調用在await
之後仍會影響到下面的調用;(async () => { const asyncArray = new AsyncArray(...ids) console.log(await asyncArray.map(i => i * 2)) // InnerAsyncArray { _arr: [ 2, 4, 6, 8 ] } console.log(await asyncArray) // InnerAsyncArray { _arr: [ 2, 4, 6, 8 ] } })()
-
併發調用的順序不能確定,會影響到內部數組,導致結果不能確定
;(async () => { const asyncArray = new AsyncArray(...ids) ;(async () => { console.log( await asyncArray.filter(async i => i % 2 === 1).map(async i => i * 2), ) // InnerAsyncArray { _arr: [ 2, 6 ] } })() ;(async () => { console.log(await asyncArray) // InnerAsyncArray { _arr: [ 2, 6 ] } })() })()
先解決第一個問題,這裏只需要判斷一下是否爲終結操作(forEach
),是的話就直接返回結果,否則繼續下一次循環
class AsyncArray {
// 其他內容...
async then(resolve, reject) {
const arr = this._parallel
? new InnerAsyncArrayParallel(this._arr)
: new InnerAsyncArray(this._arr)
let result = this._arr
for (let task of this._task) {
const temp = await arr[task.type](...task.args)
if (
temp instanceof InnerAsyncArray ||
temp instanceof InnerAsyncArrayParallel
) {
result = temp._arr
} else {
// 如果已經是終結操作就返回數組的值
if (resolve) {
resolve(temp)
}
return temp
}
}
if (resolve) {
resolve(result)
}
return result
}
}
現在,第一個問題簡單解決
;(async () => {
const asyncArray = new AsyncArray(...ids)
console.log(await asyncArray.map(i => i * 2)) // [ 2, 4, 6, 8 ]
})()
第二、第三個問題看起來似乎是同一個問題?其實我們可以按照常規思維解決第一個問題。既然 await
之後仍然會影響到下面的調用,那就在 then
中把 _task
清空好了,修改 then
函數
class AsyncArray {
// 其他內容...
async then(resolve, reject) {
const arr = this._parallel
? new InnerAsyncArrayParallel(this._arr)
: new InnerAsyncArray(this._arr)
let result = this._arr
for (let task of this._task) {
const temp = await arr[task.type](...task.args)
if (
temp instanceof InnerAsyncArray ||
temp instanceof InnerAsyncArrayParallel
) {
result = temp._arr
} else {
// 如果已經是終結操作就返回數組的值
if (resolve) {
resolve(temp)
}
this._task = []
return temp
}
}
if (resolve) {
resolve(result)
}
this._task = []
return result
}
}
現在,第一個問題解決了,但第二個問題不會解決。究其原因,還是異步事件隊列的問題,雖然 async-await
能夠讓我們以同步的方式寫異步的代碼,但千萬不可忘記它們本質上還是異步的!
;(async () => {
await Promise.all([
(async () => {
console.log(
await asyncArray.filter(async i => i % 2 === 1).map(async i => i * 2),
) // [ 2, 6 ]
})(),
(async () => {
console.log(await asyncArray) // [ 2, 6 ]
})(),
])
console.log(await asyncArray) // [ 1, 2, 3, 4 ]
})()
可以看到,在使用 await
進行等待之後就如同預期的 _task
被清空了。然而,併發執行的沒有等待的 await asyncArray
卻有奇怪的問題,因爲它是在 _task
清空之前執行的。
並且,這帶來一個副作用: 無法緩存操作了
;(async () => {
const asyncArray = new AsyncArray(...ids).map(i => i * 2)
console.log(await asyncArray) // [ 2, 4, 6, 8 ]
console.log(await asyncArray) // [ 1, 2, 3, 4 ]
})()
使用不可變數據
爲了解決直接修改內部數組造成的問題,我們可以使用不可變數據解決這個問題。試想:如果我們每次操作都返回一個新的 AsyncArray
,他們之間沒有關聯,這樣又如何呢?
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
/**
* @field 保存異步任務
* @type {Action[]}
*/
this._task = []
/**
* 是否並行化
*/
this._parallel = false
}
forEach(fn) {
return this._addTask(Action.Type.forEach, [fn])
}
map(fn) {
return this._addTask(Action.Type.map, [fn])
}
filter(fn) {
return this._addTask(Action.Type.filter, [fn])
}
parallel() {
this._parallel = true
return this
}
serial() {
this._parallel = false
return this
}
_addTask(type, args) {
const result = new AsyncArray(...this._arr)
result._task = [...this._task, new Action(type, args)]
result._parallel = this._parallel
return result
}
/**
* 終結整個鏈式操作並返回結果
*/
async then(resolve, reject) {
const arr = this._parallel
? new InnerAsyncArrayParallel(this._arr)
: new InnerAsyncArray(this._arr)
let result = this._arr
for (let task of this._task) {
const temp = await arr[task.type](...task.args)
if (
temp instanceof InnerAsyncArray ||
temp instanceof InnerAsyncArrayParallel
) {
result = temp._arr
} else {
// 如果已經是終結操作就返回數組的值
if (resolve) {
resolve(temp)
}
return temp
}
}
if (resolve) {
resolve(result)
}
return result
}
}
再次測試上面的那第三個問題,發現已經一切正常了呢
- 併發調用的順序不能確定,但不會影響內部數組了,結果是確定的
;(async () => {
const asyncArray = new AsyncArray(...ids)
await Promise.all([
(async () => {
console.log(
await asyncArray.filter(async i => i % 2 === 1).map(async i => i * 2),
) // [ 2, 6 ]
})(),
(async () => {
console.log(await asyncArray) // [ 1, 2, 3, 4 ]
})(),
])
console.log(await asyncArray) // [ 1, 2, 3, 4 ]
})()
- 操作可以被緩存
;(async () => {
const asyncArray = new AsyncArray(...ids).map(i => i * 2)
console.log(await asyncArray) // [ 2, 4, 6, 8 ]
console.log(await asyncArray) // [ 2, 4, 6, 8 ]
})()
完整代碼
下面吾輩把完整的代碼貼出來
/**
* 保存高階函數傳入的異步操作
*/
class Action {
constructor(type, args) {
/**
* @field 異步操作的類型
* @type {string}
*/
this.type = type
/**
* @field 異步操作的參數數組
* @type {Function}
*/
this.args = args
}
}
/**
* 所有的操作類型
*/
Action.Type = {
forEach: 'forEach',
map: 'map',
filter: 'filter',
}
/**
* 真正實現的異步數組
*/
class InnerAsyncArray {
constructor(arr) {
this._arr = arr
}
async forEach(fn) {
const arr = this._arr
for (let i = 0, len = arr.length; i < len; i++) {
await fn(arr[i], i, this)
}
this._arr = []
}
async map(fn) {
const arr = this._arr
const res = []
for (let i = 0, len = arr.length; i < len; i++) {
res.push(await fn(arr[i], i, this))
}
this._arr = res
return this
}
async filter(fn) {
const arr = this._arr
const res = []
for (let i = 0, len = arr.length; i < len; i++) {
if (await fn(arr[i], i, this)) {
res.push(arr[i])
}
}
this._arr = res
return this
}
}
class InnerAsyncArrayParallel {
constructor(arr) {
this._arr = arr
}
async _all(fn) {
return Promise.all(this._arr.map(fn))
}
async forEach(fn) {
await this._all(fn)
this._arr = []
}
async map(fn) {
this._arr = await this._all(fn)
return this
}
async filter(fn) {
const arr = await this._all(fn)
this._arr = this._arr.filter((v, i) => arr[i])
return this
}
}
class AsyncArray {
constructor(...args) {
this._arr = Array.from(args)
/**
* @field 保存異步任務
* @type {Action[]}
*/
this._task = []
/**
* 是否並行化
*/
this._parallel = false
}
forEach(fn) {
return this._addTask(Action.Type.forEach, [fn])
}
map(fn) {
return this._addTask(Action.Type.map, [fn])
}
filter(fn) {
return this._addTask(Action.Type.filter, [fn])
}
parallel() {
this._parallel = true
return this
}
serial() {
this._parallel = false
return this
}
_addTask(type, args) {
const result = new AsyncArray(...this._arr)
result._task = [...this._task, new Action(type, args)]
result._parallel = this._parallel
return result
}
/**
* 終結整個鏈式操作並返回結果
*/
async then(resolve, reject) {
const arr = this._parallel
? new InnerAsyncArrayParallel(this._arr)
: new InnerAsyncArray(this._arr)
let result = this._arr
for (let task of this._task) {
const temp = await arr[task.type](...task.args)
if (
temp instanceof InnerAsyncArray ||
temp instanceof InnerAsyncArrayParallel
) {
result = temp._arr
} else {
// 如果已經是終結操作就返回數組的值
if (resolve) {
resolve(temp)
}
return temp
}
}
if (resolve) {
resolve(result)
}
return result
}
}
總結
那麼,關於 JavaScript 中如何封裝一個可以使用異步操作高階函數的數組就先到這裏了,完整的 JavaScript 異步數組請參考吾輩的 AsyncArray(使用 TypeScript 編寫)。