等我生成一份500kb的json文件,看哪位小朋友还敢硬编码。

原本🐱忙着睡觉,并不想自己提供一份超大数据文件给小朋友。不过我显然高估了小朋友的读题能力——当我看到有同学重复了10+遍DOM树操作时,我差点在数据结构课上气得跳起来。

总之,感谢这位同学治好了我的低血压,我花了一个小时,粗略地认识了迭代器,顺便写了一个可以批量收集hitokoto语句的小脚本。在🎮的帮助下,增加了处理Status Code 514访问拒绝的部分,由于能够在访问频率过快时自动等待一段时间重启,获取数据所需时间显著下降👍。

Prerequisite

我完成的第一个版本使用了计时器来控制请求的发送。

1
2
const timeId = setInterval(sendRequest, 1000);
setTimeout(() => clearInterval(timeId), 1000*n);

简化代码如上,通过改变n的数量,以每秒1次的频率向目标接口发送请求,并把data加入到数组中。

很显然这是一个非常不优雅的解决方案,有一种手工计时的味道,以及没有catch可能出现的错误,使程序不太稳定。

在面向StackOverflow编程后,我意识到异步迭代+generator的组合可能比较适合这个使用场景,开始尝试。

Iterable object

这是我第一次见到可迭代对象🤦‍♂️。简而言之,iterable object中包含了我们要迭代的范围,每一轮循环中需要返回的变量,以及循环被推进和判断迭代是否结束的方法next()

下面就是一个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
let range = {
from: 1,
to: 5,//迭代返回

[Symbol.iterator]() {
this.current = this.from;
return this;//要返回的变量
},

next() {
if (this.current <= this.to) {
return { done: false, value: this.current++ };//如何推进循环
} else {
return { done: true };//迭代是否结束
}
}
};

现在,这个迭代器就可以使用for...of方法来遍历了。

1
2
3
for (let num of range) {
alert(num); //1 2 3 4 5
}

Generator

通常来说,一个function只能返回return一次,而generator可以使用yield返回任意次,直到触发了return为止。此时的donetrue

1
2
3
4
5
6
7
function* generateSequence() {
yield 1;
return 2;
}
const generator = generateSequence();
const one = generator.next();//{ value: 1, done: false }
const two = generator.next();//{ value: 2, done: true }

可以想象到的是,我们可以通过传参,yield出我们想要的结果。

1
2
3
4
5
6
7
8
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for(let value of generateSequence(1, 5)) {
alert(value); // 1 2 3 4 5
}//遍历输出

在这个例子中,已经可以举一反三,利用临时变量的累加来达到批量发送请求的目的了。

迭代!

为什么不直接使用for循环呢?在一般情况下,for循环都是同步进行的。也就是说,当循环100次时,这100次的请求是几乎同步发送的,因为我们没有要求剩下99次请求需要等待第1次请求返回后再依次发送。因此,需要利用for await来进行异步操作。那么,只需要使用generator来生成一个间隔一段时间就发送一次请求的迭代器即可。

1
2
3
4
5
6
7
async function* generateSequence(end) {
for (let i = 1; i <= end; i++) {
const response = await getData();
await new Promise((resolve) => setTimeout(resolve, 100));//每次发送请求需要等待一段时间,否则请求过快会被拒绝
yield response;
}
}

这里我返回的是response,一个json变量。最后,只需要在for await中遍历创建好的迭代器,依次将获得的json对象push入事先创建的数组之中就能完成获取。

1
2
3
let data = [];
let generator = generateSequence(100);//获取100条语句
for await (let response of generator) data.push(response);//遍历迭代器并将结果push进数组

加速

完成这样一个循环后,程序已经可以运行了。但是由于服务器和网络等因素的限制,如果请求速度过快(如500ms/次),就有可能出现Status code 514的错误。如果此时不处理这个错误,程序将直接退出。这显然不是一个稳定、快速的脚本,因此我在迭代器中增加了一些错误处理,当请求失败时,等待一段时间再重新进行请求。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
const getData = async () => {
try {
const response = await axios.get('https://v1.hitokoto.cn', {
params: {
encode: 'json',
},
});
console.log('👍已获取数据!');
return await response.data;
} catch (e) {
console.log('🙀访问拒绝,等待中...');
await sleep(1000);
console.log('🐱重新获取数据...');
return {};
}
};

async function* generateSequence(end) {
for (let i = 1; i <= end; i++) {
let response = {};
while (JSON.stringify(response) === '{}') {//当请求失败时,getData将等待1s后返回{},此时重启请求
response = await getData();
}
await sleep(200);//等待200ms
yield response;
}
}

增加容错之后,相比于第一个版本1s/次的速度,获取数据的效率显著上升。

太棒了!学到许多👍