讨论还请到原 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 帮我从算法层面上进行了屡次优化,在此很是感谢~~