collection-query
collection-query 是一个集合操作类库,它把数据集合分成 pull 和 push 两种类型,再用同样语义的方法去操作它们。下文简称 cq 。
普通的数组操作
介绍 cq 之前,先看一下普通的数组操作。
// 据说变量名长度小于7便于阅读
// 统计一下全局有多少个这样的变量
const x = Object.keys(globalThis) // 获取数据的集合
.map((x) => x.length) // 变换
.filter((x) => x < 7) // 变换
.reduce((r, _) => r + 1, 0); // 聚合
console.log(x);
codesandbox xx.length
在这里,数组的操作可以简单分为3个步骤。
- 获取数据的集合。
- 把集合变换为另一个集合。
- 把集合聚合成一个值。
cq 的集合操作
接下来,用 cq 复刻上一节的数组操作。
import { transfer } from "collection-query";
import { createFrom, map, filter, reduce } from "collection-query/pull";
// 获取数据的集合
const s = createFrom(Object.keys(globalThis));
// 变换
const s2 = transfer(s, [
map((x: string) => x.length),
filter((x: number) => x < 7),
]);
// 聚合
const x = reduce(s2, (r, _) => r + 1, 0);
console.log(x);
其实这个例子就揭示了 cq 的主要用法。
- 确定集合的类型是 pull 或 push ,选择对应的方法集。
- 把集合变换为另一个集合。
- 把集合聚合成一个值或消费。
相比之下
相比之下,这个例子的代码结构显得冗余,而这是 cq 对环境的妥协。
- 不采用链式操作,是为了打包时支持 tree sharking ,减少体积。
- 使用
transfer(s, [...methods])
这种形式,是为了支持 ts 的类型推导。如:上文的const x...
最终能推导为 number 类型。
pull 和 push
初步了解 cq 后,再回来看下数据的 pull 和 push 。
- pull ,数据被动生产,代码主动消费。
- push ,数据主动生产,代码被动消费。
rxjs 就介绍得很好,cq 也受此启发。相关文档
PullStream
之前 cq 的例子采用的正是 pull ,确切来说是 PullStream 。
cq 的 PullStream 实际上是原生对象 Generator Function 的别名。(与生成器相关)
原生数组也是一个 pull 类型的数据,但是用 PullStream 可以更抽象地去描述数据。
import { transfer } from "collection-query";
// 采用 PullStream 的方法集
import { take, map, filter, count } from "collection-query/pull";
// 获取数据的集合;生成一个代表自然数的 PullStream
const s = function* () {
let count = 0;
while (true) {
yield count++;
}
};
// 变换;前100个数里能被3整除的数
const s2 = transfer(s, [take(100), map((x) => x % 3), filter((x) => x === 0)]);
// 聚合;求元素的个数
const x = count(s2);
console.log(x);
- 用原生语法“星号函数”就能方便地创建 PullStream。
- PullStream 是 lazy 的,在聚合或消费前都不会产生数据,可以节省一定的内存空间。
- PullStream 可以轻易地表达数组不能表达的不限长度的数据。
AsyncPullStream
AsyncPullStream 是 PullStream 的异步版本,对应的原生对象是 Async Generator Function 。(与 for await...of 相关)
// 感谢 cnodejs 的 api 支持跨域
// 这里统计一下在首页发帖的用户,他们的收藏情况
import { transfer } from "collection-query";
// 采用 AsyncPullStream 的方法集
import { flatten, map, forEach } from "collection-query/async-pull";
// 获取数据的集合
const s = async function* () {
for (let page = 1; page <= 2; page++) {
// 请求首页收据,每次10条
const resp = await fetch(
`https://cnodejs.org/api/v1/topics?mdrender=false&&limit=10&&page=${page}`
);
const data = await resp.json();
yield data.data;
}
};
// 变换
const s2 = transfer(s, [
flatten,
map((x) => x.author.loginname),
map(async (x) => {
// 请求某个用户的收藏
const resp = await fetch(`https://cnodejs.org/api/v1/topic_collect/${x}`);
const data = await resp.json();
return data.data;
}),
flatten,
map((x) => x.title),
]);
// 消费
// for await (const x of s2()) {
// console.log(x);
// }
forEach(s2, (x) => console.log(x));
cq 操作 AsyncPullStream 只是采用了不同的方法集,流程与之前操作 PullStream 一致。
PushStream 和 AsyncPushStream
而 cq 对应的 push 类型实现是 PushStream 和 AsyncPushStream ,它们不是原生对象,这里先不赘述它们的结构。
groupBy
就说 PushStream 最有趣的一点,它能表达 lazy 的 groupBy 。
当一个集合 s 经过 groupBy 变换后,我们会希望新的集合是一个元素为 [key, group_s]
的集合。
// 假如能牺牲一些打包空间,链式调用还挺优雅的。
s.groupBy(keyOf)
.map((x) => {
const [key, group_s] = x;
// group_s 也是一个集合
const count = group_s.filter(pred).count();
return [key, count];
})
.forEach((x) => console.log(x));
如果此时的 s 是 PullStream ,那么 groupBy 后 map 第一个元素就会遍历 s 的所有内容,这会失去流式处理数据的空间优势。
如果此时的 s 是 PushStream ,那么被动消费的 PushStream 可以通过异步操作等待 group_s 的消费返回,不需要提前遍历 s 的所有内容。
以下是 PullStream groupBy 的实例。
// 统计 cnodejs 首页各个 tab 回复大于0的帖子数量。
import { transfer, EmitType } from "collection-query";
// 采用 PushStream 的方法集
import {
create,
flatten,
groupBy,
map,
count,
filter,
incubate,
forEach,
} from "collection-query/push";
// 获得数据的集合
const s = create(async (emit) => {
// 请求首页收据
const resp = await fetch("https://cnodejs.org/api/v1/topics?mdrender=false");
const data = await resp.json();
emit(EmitType.Next, data.data);
emit(EmitType.Complete);
});
// 变换
const s2 = transfer(s, [
flatten,
groupBy((x) => x.tab),
map(async (x) => {
// 获得数据的集合
const [key, group_s] = x;
// 变换
const s2 = transfer(group_s, [
map((x) => x.reply_count),
filter((x) => 0 < x),
]);
// 聚合
const _count = await count(s2);
return [key, _count];
}),
incubate,
]);
// 消费
forEach(s2, (x) => console.log(x));
写到最后
cq 的介绍到此结束了,这里复述一遍 cq 的主要用法。
- 确定集合的类型,选择对应的方法集。
- 把集合变换为另一个集合。
- 把集合聚合成一个值或消费。