原本🐱忙着睡觉,并不想自己提供一份超大数据文件给小朋友。不过我显然高估了小朋友的读题能力——当我看到有同学重复了10+遍DOM树操作时,我差点在数据结构课上气得跳起来。
总之,感谢这位同学治好了我的低血压,我花了一个小时,粗略地认识了迭代器,顺便写了一个可以批量收集hitokoto语句的小脚本。在🎮的帮助下,增加了处理Status Code 514
访问拒绝的部分,由于能够在访问频率过快时自动等待一段时间重启,获取数据所需时间显著下降👍。
Prerequisite
我完成的第一个版本使用了计时器来控制请求的发送。
javascript复制代码1const timeId = setInterval(sendRequest, 1000);
2setTimeout(() => clearInterval(timeId), 1000*n);
简化代码如上,通过改变n的数量,以每秒1次的频率向目标接口发送请求,并把data
加入到数组中。
很显然这是一个非常不优雅的解决方案,有一种手工计时的味道,以及没有catch可能出现的错误,使程序不太稳定。
在面向StackOverflow编程后,我意识到异步迭代+generator的组合可能比较适合这个使用场景,开始尝试。
Iterable object
这是我第一次见到可迭代对象🤦♂️。简而言之,iterable object中包含了我们要迭代的范围,每一轮循环中需要返回的变量,以及循环被推进和判断迭代是否结束的方法next()
。
下面就是一个例子:
javascript复制代码1let range = {
2 from: 1,
3 to: 5,//迭代返回
4
5 [Symbol.iterator]() {
6 this.current = this.from;
7 return this;//要返回的变量
8 },
9
10 next() {
11 if (this.current <= this.to) {
12 return { done: false, value: this.current++ };//如何推进循环
13 } else {
14 return { done: true };//迭代是否结束
15 }
16 }
17};
现在,这个迭代器就可以使用for...of
方法来遍历了。
javascript复制代码
1for (let num of range) { 2 alert(num); //1 2 3 4 5 3}
Generator
通常来说,一个function只能返回return
一次,而generator
可以使用yield
返回任意次,直到触发了return
为止。此时的done
为true
。
javascript复制代码1function* generateSequence() {
2 yield 1;
3 return 2;
4}
5const generator = generateSequence();
6const one = generator.next();//{ value: 1, done: false }
7const two = generator.next();//{ value: 2, done: true }
可以想象到的是,我们可以通过传参,yield
出我们想要的结果。
javascript复制代码1function* generateSequence(start, end) {
2 for (let i = start; i <= end; i++) {
3 yield i;
4 }
5}
6for(let value of generateSequence(1, 5)) {
7 alert(value); // 1 2 3 4 5
8}//遍历输出
在这个例子中,已经可以举一反三,利用临时变量的累加来达到批量发送请求的目的了。
迭代!
为什么不直接使用for循环呢?在一般情况下,for循环都是同步进行的。也就是说,当循环100次时,这100次的请求是几乎同步发送的,因为我们没有要求剩下99次请求需要等待第1次请求返回后再依次发送。因此,需要利用for await
来进行异步操作。那么,只需要使用generator来生成一个间隔一段时间就发送一次请求的迭代器即可。
javascript复制代码1async function* generateSequence(end) {
2 for (let i = 1; i <= end; i++) {
3 const response = await getData();
4 await new Promise((resolve) => setTimeout(resolve, 100));//每次发送请求需要等待一段时间,否则请求过快会被拒绝
5 yield response;
6 }
7}
这里我返回的是response,一个json变量。最后,只需要在for await
中遍历创建好的迭代器,依次将获得的json对象push
入事先创建的数组之中就能完成获取。
javascript复制代码
1let data = []; 2let generator = generateSequence(100);//获取100条语句 3for await (let response of generator) data.push(response);//遍历迭代器并将结果push进数组
加速
完成这样一个循环后,程序已经可以运行了。但是由于服务器和网络等因素的限制,如果请求速度过快(如500ms/次),就有可能出现Status code 514
的错误。如果此时不处理这个错误,程序将直接退出。这显然不是一个稳定、快速的脚本,因此我在迭代器中增加了一些错误处理,当请求失败时,等待一段时间再重新进行请求。
javascript复制代码1const getData = async () => {
2 try {
3 const response = await axios.get('https://v1.hitokoto.cn', {
4 params: {
5 encode: 'json',
6 },
7 });
8 console.log('👍已获取数据!');
9 return await response.data;
10 } catch (e) {
11 console.log('🙀访问拒绝,等待中...');
12 await sleep(1000);
13 console.log('🐱重新获取数据...');
14 return {};
15 }
16};
17
18async function* generateSequence(end) {
19 for (let i = 1; i <= end; i++) {
20 let response = {};
21 while (JSON.stringify(response) === '{}') {//当请求失败时,getData将等待1s后返回{},此时重启请求
22 response = await getData();
23 }
24 await sleep(200);//等待200ms
25 yield response;
26 }
27}
增加容错之后,相比于第一个版本1s/次的速度,获取数据的效率显著上升。
太棒了!学到许多👍