摘要:第二種則一定會執行所有的異步函數,即便你需要使用的是這些高階函數。并發實現的異步數組然后修改,使用即可上面的其他內容終結整個鏈式操作并返回結果這里使用是為了兼容的調用方式調用方式不變。
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 編寫)。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/104944.html
摘要:的執行與狀態無關當得到狀態不論成功或失敗后就會執行,原文鏈接參考鏈接對象 同期異步系列文章推薦談一談javascript異步javascript異步中的回調javascript異步與promisejavascript異步之Promise.resolve()、Promise.reject()javascript異步之Promise then和catchjavascript異步之async...
摘要:本次的任務假如。。。。。引擎發生了重大故障,方法變成了,為了拯救世界,需要開發一個模塊來解決此問題。實現首先要知道是什么是對異步編程的一種抽象。數組中任何一個為的話,則整個調用會立即終止,并返回一個的新的對象。 本次的任務 假如。。。。。 JavaScript v8 引擎發生了重大故障,Promise.all 方法變成了 undefined ,為了拯救 JavaScript 世界,需要...
摘要:年月日更新后來在編程過程中發現用會更加方便。如果是沒辦法應對異步。重新調了一下,發現幾點寫下來異步操作這里的回調函數一定要寫成這樣的形式,如果使用的是這樣的形式會指向這個匿名函數。 2017年7月20日更新 后來在編程過程中發現用iterator會更加方便。在Array的iteration方法里面有這么一個:Array.prototype[@@iterator]()。用法是`arr[S...
摘要:回調函數是的一大特色官方的基本都是以會回調方式傳遞函數返回值。針對這種普遍問題,應勢而生基本用法創建做一些異步操作的事情,然后一切正常的構造器接受一個函數作為參數,它會傳遞給這個回調函數兩個變量和。 Promise 是什么? Promise 對象用來進行延遲(deferred) 和 異步(asynchronous) 計算。 一個 Promise 處于以下三種狀態之一: pend...
摘要:這就是積極的函數式編程。上一章翻譯連載第章遞歸下輕量級函數式編程你不知道的姊妹篇原創新書移動前端高效開發實戰已在亞馬遜京東當當開售。 原文地址:Functional-Light-JS 原文作者:Kyle Simpson-《You-Dont-Know-JS》作者 關于譯者:這是一個流淌著滬江血液的純粹工程:認真,是 HTML 最堅實的梁柱;分享,是 CSS 里最閃耀的一瞥;總...
閱讀 1264·2021-09-23 11:51
閱讀 1369·2021-09-04 16:45
閱讀 626·2019-08-30 15:54
閱讀 2076·2019-08-30 15:52
閱讀 1594·2019-08-30 11:17
閱讀 3098·2019-08-29 13:59
閱讀 2010·2019-08-28 18:09
閱讀 381·2019-08-26 12:15