異步迭代器在業務中的實踐

討論還請到原 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

需求老是會變的

假設不久以後,咱們接到了一個新的需求,咱們業務中的某兩個(或者三個、四個)類別的列表須要在同一個頁面上展現。也就是說,數據的映射關係,發生了以下改變:數組

image

方案設計

讓咱們先思考一下:如何去合併列表數據,讓咱們的列表還能像以前同樣保證有序?爲了方便討論,我在這裏抽象出兩個數據源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 幫我從算法層面上進行了屢次優化,在此很是感謝~~

結語

  • 還請注意,若是是有跳頁需求的話,就不能這麼封裝了
  • 除了更好的抽象帶來的可讀性,代碼也變得更加容易測試了
相關文章
相關標籤/搜索