资讯专栏INFORMATION COLUMN

异步迭代器在业务中的实践

Flands / 1073人阅读

摘要:讨论还请到原下什么是异步迭代器关注或者通过其他渠道关注发展的同学应该早已注意到了一个新的草案。这项草案就是我本文中,我将要提到的异步迭代器。因此我去学习异步迭代器,自然也是为了解决我在业务中所遇到的问题。

讨论还请到原 github issue 下:https://github.com/LeuisKen/l...
什么是异步迭代器

关注tc39或者通过其他渠道关注JavaScript发展的同学应该早已注意到了一个新的草案:proposal-async-iteration。该草案在本文成文时,已经进入了ECMAScript® 2019规范,也就是说,成为了JavaScript语言本身的一部分。这项草案就是我本文中,我将要提到的异步迭代器(Asynchronous Iterators)

这个新的语法,为之前的生成器函数(generator function)提供了异步的能力。举个例子,就是下面这样。

// 之前的生成器函数
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);
    }
}
业务场景

我们学习新的东西,必然是要伴随着业务价值的。因此我去学习异步迭代器,自然也是为了解决我在业务中所遇到的问题。接下来我来分享一个场景:

在移动端,经常会有滑到页面底部,加载更多的场景。比如,我们在浏览新闻的时候,选择一个分类,就能看到对应分类的很多新闻,这些新闻通常是新的在前,旧的在后,顺序的排列下来。例如,百度新闻:https://news.baidu.com/news#/

本质上,这是一个分页器。通常的实现是,前端向服务端发送一个带有指定类别、指定页码(或者时间戳)的数据请求,服务端返回一个数据列表,该列表长度通常是固定的。然后前端在拿到这部分数据后,将数据渲染到视图上。值得我们注意的是,在这个场景下,因为是用户滑动到底部,触发对下一页的加载,所以是不存在从第一页跳到第五页这种跳页的需求的。

我们也许会用这样的代码来实现这个需求:

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、异步竞态),这段代码虽然不甚优雅,但是足够实现我们的业务需求了。

需求总是会变的

假设不久之后,我们接到了一个新的需求,我们业务中的某两个(或者三个、四个)类别的列表需要在同一个页面上展示。也就是说,数据的映射关系,发生了如下改变:

方案设计

让我们先思考一下:如何去合并列表数据,让我们的列表还能像之前一样保证有序?为了方便讨论,我在这里抽象出两个数据源A、B,他们里面的内容是两个有序数组,如下所示:

A ---> [1, 3, 5, 7, 9, 11, …]
B ---> [0, 2, 4, 6, 8, …]

那么我们预期的合并后列表就是:

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 + `
  • Repo: ${next.repository_url}
    ` + `
    Title: ${next.title}
    ` + `
    Time: ${next.created_at}
    `, ""); });
  • 再设计

    有了异步迭代器的抽象,我们重新来看看我们的设计,相信大家心中都有了答案:

    /**
     * 合并多个异步迭代器,返回一个新的异步迭代器
     * 该迭代器每次返回 pickNumber 个数据
     * 数据按照 sortFn 排序
     *
     * @param {Array} iterators 异步迭代器数组对象
     * @param {Function} sortFn 对请求结果进行排序的函数
     * @param {number} pickNumber 迭代器每次返回的元素数量
     * @return {Iterator} 合并后的异步迭代器
     */
    export default async function* mixLoader(iterators, sortFn, pickNumber) {
    
    }
    实现

    mixLoader取意是混合的加载器(老实说,并不是一个非常合适的名字),这个函数我做了一版最简单的实现,后续 @STLighter 帮我从算法层面上进行了多次优化,在此非常感谢~~

    github仓库地址:https://github.com/LeuisKen/m...

    第一版实现(虽然实现的不好但是好在原理简单):https://github.com/LeuisKen/m...

    @STLighter 优化后的实现:https://github.com/LeuisKen/m...

    结语

    还请注意,如果是有跳页需求的话,就不能这么封装了

    除了更好的抽象带来的可读性,代码也变得更加容易测试了

    文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

    转载请注明本文地址:https://www.ucloud.cn/yun/94441.html

    相关文章

    • js设计模式--迭代器模式

      摘要:文章系列设计模式单例模式设计模式策略模式设计模式代理模式概念迭代器模式是指提供一种方法顺序访问一个聚合对象中的各个元素,而又不需要暴露该对象的内部表示。 前言 本系列文章主要根据《JavaScript设计模式与开发实践》整理而来,其中会加入了一些自己的思考。希望对大家有所帮助。 文章系列 js设计模式--单例模式 js设计模式--策略模式 js设计模式--代理模式 概念 迭代器模式是指...

      binta 评论0 收藏0
    • ES6——生成器

      摘要:我们还能如何使用生成器作为迭代器的能力使对象可迭代。一些重要的事件值得了解生成器是由布伦丹艾希首次在上实现的。布伦丹艾希的设计是紧紧跟随由启发的生成器。 什么是生成器? 我们先从下面的这里例子开始。 function* quips(name) { yield hello + name + !; yield i hope you are enjoying the blog po...

      cgh1999520 评论0 收藏0
    • Python:Tornado 第一章:异步及协程基础:第二节:Python关键字yield

      摘要:在种,使用关键字定义的迭代器也被称为生成器迭代器迭代器是访问集合内元素的一种方式。调用任何定义包含关键字的函数都不会执行该函数,而是会获得一个队应于该函数的迭代器。 上一篇文章:Python:Tornado 第一章:异步及协程基础:第一节:同步与异步I/O下一篇文章:Python:Tornado 第一章:异步及协程基础:第三节:协程 协程是Tornado中进行异步I/O代码开发的方法...

      reclay 评论0 收藏0

    发表评论

    0条评论

    Flands

    |高级讲师

    TA的文章

    阅读更多
    最新活动
    阅读需要支付1元查看
    <