討論還請到原 github issue 下: https://github.com/LeuisKen/l...
關注tc39或者經過其餘渠道關注JavaScript發展的同窗應該早已注意到了一個新的草案:proposal-async-iteration。該草案在本文成文時,已經進入了ECMAScript® 2019規範,也就是說,成爲了JavaScript語言自己的一部分。這項草案就是我本文中,我將要提到的異步迭代器(Asynchronous Iterators)
。前端
這個新的語法,爲以前的生成器函數(generator function)提供了異步的能力。舉個例子,就是下面這樣。react
// 以前的生成器函數 function* sampleGenerator(array) { for (let i = 0; i < array.length; i++) { yield array[i]; } } // 如今的異步生成器函數,讓咱們能夠在生成器函數前面加上 async 關鍵字 async function* sampleAsyncGenerator(getItemByPageNumber, totalPages) { for (let i = 0; i < totalPages; i++) { // 這樣咱們就能在裏面使用 await 了 yield await getItemByPageNumber(i); } }
咱們學習新的東西,必然是要伴隨着業務價值的。所以我去學習異步迭代器,天然也是爲了解決我在業務中所遇到的問題。接下來我來分享一個場景:git
在移動端,常常會有滑到頁面底部,加載更多的場景。好比,咱們在瀏覽新聞的時候,選擇一個分類,就能看到對應分類的不少新聞,這些新聞一般是新的在前,舊的在後,順序的排列下來。例如,百度新聞:https://news.baidu.com/news#/github
本質上,這是一個分頁器。一般的實現是,前端向服務端發送一個帶有指定類別、指定頁碼(或者時間戳)的數據請求,服務端返回一個數據列表,該列表長度一般是固定的。而後前端在拿到這部分數據後,將數據渲染到視圖上。值得咱們注意的是,在這個場景下,由於是用戶滑動到底部,觸發對下一頁的加載,因此是不存在從第一頁跳到第五頁這種跳頁的需求的。ajax
咱們也許會用這樣的代碼來實現這個需求:算法
let page = 1; // 從第一頁開始 let isLastPage = false; function getPage(type) { $.ajax({ url: '/api/list', data: { page, type }, success(res) { isLastPage = res.isLastPage; // 是否爲最後頁 // 根據 res 更新視圖 page++; } }) } // 用戶觸發加載的事件處理函數 function handleLoadEvent() { if (isLastPage) { return; } getPage('推薦'); }
不去管一些其餘的實現細節(如,throttle、異步競態),這段代碼雖然不甚優雅,可是足夠實現咱們的業務需求了。api
假設不久以後,咱們接到了一個新的需求,咱們業務中的某兩個(或者三個、四個)類別的列表須要在同一個頁面上展現。也就是說,數據的映射關係,發生了以下改變:數組
讓咱們先思考一下:如何去合併列表數據,讓咱們的列表還能像以前同樣保證有序?爲了方便討論,我在這裏抽象出兩個數據源A、B,他們裏面的內容是兩個有序數組,以下所示:異步
A ---> [1, 3, 5, 7, 9, 11, …] B ---> [0, 2, 4, 6, 8, …]
那麼咱們預期的合併後列表就是:async
merged ---> [0, 1, 2, 3, 4, 5, 6, …]
假設咱們每次分頁去取數據,預期的數據長度(記爲:pickNumber)是3,那麼咱們在第一次取數據後,回調中預期請求到的值就是[0, 1, 2]
。那麼若是咱們從A中拿3個,B中也拿3個,那麼排序後,從排序的結果中取3個,就拿到了咱們想要的[0, 1, 2]
。要取出合併後列表中有序的pickNumber
個數據,就先從各個數據源中取pickNumber
個數據,而後對結果排序,取出前pickNumber
個數據,這就是我所選擇的保證數據有序的策略。
這個策略,在一些極限狀況下,好比合並後列表的前幾頁都是A等等,都是能夠保證順序的。
方案肯定後,咱們來設計下咱們要實現的函數,很天然的,咱們會想到這樣的實現:
/** * 從多個 type 列表中獲取數據 * * @param {Array} types 須要合併的 type 列表 * @param {Function} sortFn 排序函數 * @param {number} pickNumber 每頁須要的數據 * @param {Function} callback 返回頁數據的回調函數 */ function getListFromMultiTypes(types, sortFn, pickNumber, callback) { }
這樣的實現,作出來其實也是能夠知足業務需求的,可是他不是我想要的。由於type
這個東西和業務耦合的太嚴重了。固然,我能夠把types
改爲urls
,可是這種程度的抽象,仍是須要咱們把$.ajax
這個東西內置到咱們的函數裏,而我想要的僅僅只是一個merge
。因此,咱們仍是須要去追求更好的形式來抽象這個業務。
下面我把前面的A和B換一種形式組織起來,若是咱們忽略掉他們實際上是異步的東西的話,其實他們能夠被抽象爲二維數組:
// A [ [1, 3, 5], [7, 9, 11], … ] // B [ [0, 2, 4], [6, 8, 10], … ]
抽象成了二維數組,咱們能夠發現只要去迭代A、B,咱們就能夠得到想要的數據了。也就是說,A和B其實就是兩個不一樣的迭代器。加上異步的話,那麼一個分頁的服務端列表數據源,在前端能夠抽象成一個異步的迭代器,這樣抽象後,個人需求,就變成了把兩個數組merge
一下就ok了~
咱們能夠用Promise
將$.ajax
的邏輯封裝一下:
/** * 請求數據,返回 Promise * * @param {string} url 請求的 url * @param {Object} data 請求所帶的 query 參數 * @return {Promise} 用於處理請求的 Promise 對象 */ function getData(url, data) { return new Promise(function (resolve, reject) { $.ajax({ url, type: 'GET', data, success: resolve }); }); }
這樣,一個分頁器的異步生成器函數就能夠用以下代碼實現:
/** * 獲取 github 某倉庫的 issue 列表 * * @param {string} location 倉庫路徑,如:facebook/react */ async function* getRepoIssue(location) { let page = 1; let isLastPage = false; while (!isLastPage) { let lastRes = await getData( '/api/issues', {location, page} ); isLastPage = lastRes.length < PAGE_SIZE; page++; yield lastRes; } }
使用起來能夠說是很是簡單了:
const list = getRepoIssue('facebook/react'); btn.addEventListener('click', async function () { const {value, done} = await list.next(); if (done) { return; } container.innerHTML += value.reduce((cur, next) => cur + `<li><div>Repo: ${next.repository_url}</div>` + `<div>Title: ${next.title}</div>` + `<div>Time: ${next.created_at}</div>`, ''); });
有了異步迭代器的抽象,咱們從新來看看咱們的設計,相信你們心中都有了答案:
/** * 合併多個異步迭代器,返回一個新的異步迭代器 * 該迭代器每次返回 pickNumber 個數據 * 數據按照 sortFn 排序 * * @param {Array} iterators 異步迭代器數組對象 * @param {Function} sortFn 對請求結果進行排序的函數 * @param {number} pickNumber 迭代器每次返回的元素數量 * @return {Iterator} 合併後的異步迭代器 */ export default async function* mixLoader(iterators, sortFn, pickNumber) { }
mixLoader
取意是混合的加載器(老實說,並非一個很是合適的名字),這個函數我作了一版最簡單的實現,後續 @STLighter 幫我從算法層面上進行了屢次優化,在此很是感謝~~