Generator
常规函数只返回一个单一值,或不返回任何值。
而生成器函数可以按需的一个接一个返回多个值,通过yield
向外透出值,且可以与 iterable
完美结合
语法
function* gen() {
yield 1;
yield 2;
return 3
}
// 调用该函数,不会运行其中的代码,而是返回一个具有next函数的对象,通过next来运行函数中的代码
// next调用产出的结果是一个对象{value: xx, done: boolean}
let g = gen() // 持有generator对象,并未运行
let one = g.next() // 第一步yield 1, 返回 {value: 1, done: false}
let two = g.next() // 第二步yield 2, 返回 {value: 2, done: false}
let three = g.next() // 第三步yield 3, 返回 {value: 3, done: true}
可迭代
生成器调用后返回的是一个可迭代的对象,所以我们可以使用for of
来遍历(自动执行)
注意:凡是可迭代的对象,都可以用for of来遍历。
function* gen() {
yield 1;
yield 2;
return 3
}
for(let v of gen()) {
console.log(v) // 1, 2 注意!没有3,跟上面手动运行next不同
}
没有3是因为for of
会忽略done: true
时的最后一个value,我们可以将return
改为yield
,这样就可以拿到3。
因为可迭代,我们就可以使用Spread语法。
function *gen() {
yield 1
yield 2
yield 3
}
let arr = [0, ...gen()] // 0, 1, 2, 3 同样,将return改为yield才能有3
使用generator进行迭代
// 复习下:可以被for of的对象,称为可迭代对象
// 我们可以通过Symbol.iterator来实现一个对象的迭代函数,for of会去调用它
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return {
c: this.from,
l: this.to,
next() {
if(this.c <= this.l) {
// 这里this.c++ 先赋值给value了,再+
return {value: this.c++, done: false}
} else {
return {done: true}
}
}
}
}
}
[...range] // [1,2,3,4,5]
改写成generator
let range = {
from: 1,
to: 5,
[Symbol.iterator]* () {
for(let v = this.from; v <= this.to; v ++) {
yield v
}
}
}
[...range] // [1,2,3,4,5] 比上面自己实现的具备next的iterator更简短
generator组合
一个特殊功能,允许多个生成器透明的组合起来
// 一个生成数字的生成器,给定起始数字
function *genNum(start, end) {
for(let i = start; i <= end; i++) {
yield i
}
}
// 我们可以利用上面来生成code码,然后code码转成字符
function *genPassword() {
yield *genNum(48, 57)
yield *genNum(65, 90)
yield *genNum(97, 122)
}
let str = ''
for(let code of genPassword()) {
// 可以看出genNum将它生成的所有数字,都透明的吐给了genPassword生成器
str+=String.fromCharCode(code)
}
// 上面类似
function *genPassword() {
for(let i = 48; i <= 57; i++) {
yield i
}
for(let i = 65; i <= 90; i++) {
yield i
}
for(let i = 97; i <= 122; i++) {
yield i
}
}
let str = ''
for(let num of genPassword()) {
str += String.fromCharCode(num)
}
yield 是一条双向路
即可以向外吐出值,也可以接收外部的值。
let 从外部接收的返回值作为yield调用的返回值 = yield 吐到外面的值
// yield question 对外扔出question
// generator.next(4) 对question作出回答,扔进generator中,作为 从外部接收的返回值作为yield调用的返回值
function *gen() {
let res = yield '2+2=?'
alert(res)
}
let generator = gen()
// 这里,第一个next(入参),入参会被忽略掉。
generator.next(4) {value: '2+2=?', done: false}
generator.next(4) // 弹出4
注意:生成器的第一个next(arg)调用,如果传入了arg,会忽略掉
// 一个复杂点的例子
function* gen() {
let ask1 = yield "2 + 2 = ?";
alert(ask1); // 4
let ask2 = yield "3 * 3 = ?"
alert(ask2); // 9
}
let g = gen();
g.next() // {value: '2+2=?', done: false}
g.next(4) // 传递给当前yield的返回值,赋值给ask1,弹出4 {value: '3*3=?', done: false}
g.next(9) // 9赋值给ask2,弹出9,{value: undefined, done: true}
generator.throw
外部代码可能也会抛出错误,错误error也是一种值,作为yield的结果,可以使用。
此时需要用到throw这个api
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
alert("The execution does not reach here, because the exception is thrown above");
} catch(e) {
// 这里被捕获到
alert(e); // 显示这个 error
}
}
let generator = gen();
let question = generator.next().value;
generator.throw(new Error('error啦')) // 这里抛出的错误
如果你遗漏了错误捕获,错误将从generator中掉到调用地的代码中
// 你也可以在调用地捕获
function* generate() {
let result = yield "2 + 2 = ?"; // 这行出现 error
}
let generator = generate();
let question = generator.next().value;
try {
generator.throw(new Error("The answer is not found in my database"));
} catch(e) {
alert(e); // 显示这个 error
}
总结
- 通过
function *f() {}
创建的生成器函数 - 生成器函数调用时,返回一个可迭代的对象,具有
next
方法来控制运行流程。- 因为是可迭代的,因此可以配合Spread语法使用。
- 使用
yield
我们可以向外部吐出值,使用next(arg)
我们可以向生成器内部传递值,传递给当前yield,作为其返回值。function *f() {
const name = yield 'your name?'
console.log(name)
}
// 运行生成器得到可迭代对象
const generator = f()
generator.next() // 正式开始运行生成器内部代码,第一个yield被执行,向外吐出 {value: 'your name?', done: false}
generator.next('jack') // 继续执行,并传递参数给
异步迭代和Generator
回顾下普通可迭代对象
```javascript let obj = { from: 1, to: 5 }
// 希望对其使用for of迭代输出1到5. // 给其添加一个名为Symbol.iterator的特殊方法,当使用for of时,会调用该方法,此方法应该返回一个具有next方法的对象 // 每次迭代都会调用next方法,next方法返回一个具有value和done的对象,done=true时代表结束,不在迭代
let obj = { from: 1, to: 5, Symbol.iterator { return { current: this.from, last: this.to, next() { // 如果这里写成,会怎么样? // this.current < this.last // value: ++this.current // 只会输出2,3,4,5了。因为++在前,先+后赋值给value。 if(this.current <= this.last) { return {done: false, value: this.current++} } else { return {done: true} } } } } }
// 我们也能用while来写写 // 这里可以使用return,因为next是个函数,不是单纯的while循环 next() { while(this.current <= this.last) { return {done: false, value: this.current++} } return {done: true} }
// 用for循环也行,但是step语句不会执行,写在body体内就OK next() { // step省略 for(; this.current <= this.last;) { // 这里value值,先赋值,后+ return {done: false, value: this.current++} } return {done: true} }
上面的迭代都是同步的逻辑,我们来看下异步的。
1. 异步我们使用`Symbol.asyncIterator`取代`Symbol.iterator`
1. next函数需要返回一个promise值,这个我们可以使用async,天然返回一个promise
1. 使用`for await (let item of iterable)`来循环异步可迭代对象
我们来试下异步可迭代对象
```javascript
const delay = function(t) {
return new Promise(resolve => setTimeout(() => {
resolve(true)}, t))
}
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
// 第二点
async next() {
await delay(1000)
if(this.current <= this.last) {
// 返回值会被包装成resolved promise
return {done: false, value: this.current++}
} else {
return {done: true}
}
}
}
}
}
// 开始迭代它
async function t () => {
for await (let v of range) {console.log(v)}
}
t() // 间隔1秒打印1 2 3 4 5
与同步的可迭代对象不同,不能使用Spread语法,因为其只会去找[Symbol.iterator]
,如果你不用for await
一样不行。
回顾下generator
generator
函数可以生成iterator
可迭代对象,所以我们可以用for of
来循环
function *gen(start, end) {
for(let i = start; i <= end; i++) {
yield i
}
}
for(let v of gen(1, 5)) {console.log(v)} // 1, 2, 3, 4, 5
// 简短的写法来让一个对象可迭代
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() {
for(let v = this.from; v <= this.to; v++) {
yield v
}
}
}
for(let v of range) {console.log(v)} // 1,2,3,4,5
我们来看看异步的吧,加上async就可以使其成为异步生成器
const delay = function(t) {
return new Promise(resolve => setTimeout(() => {
resolve(true)}, t))
}
async function *gen(start, end) {
for(let i = start; i <= end; i++) {
await delay(1000)
yield i
}
}
// 使用for await,所以要包在async里
(async () => {
let generator = generateSequence(1, 5);
for await (let value of generator) {
alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟)
}
// for await 相当于在迭代器内部这样循环调用await next()
})();
将对象变成异步可迭代对象
let range = {
from: 1,
to: 5,
// async包装成异步逻辑
// *使用生成器函数
async *[Symbol.asyncIterator]() {
for(let v = this.from; v <= this.to; v++) {
// 延迟
await delay(1000)
// 吐值
yield value
}
}
}
(async () => {
for await (let value of range) {
alert(value); // 1,然后 2,然后 3,然后 4,然后 5
}
})();
一个实用的例子
翻页数据通常会去点击它才进行翻页,这里我们实现一个函数,可以自动拉取github上的commit(分页)。
用法
for await (let commit of fetchCommits('someone/repo)) {
// 处理commit
}
通过异步迭代来获取
github的这个接口调用后,会将下一页的数据查询链接放在响应头Link
里,具有一定格式,我们可以在调取下一页数据时使用
async function* fetchCommits(repo) {
// 构造初始仓库commit接口地址
let url = `https://api.github.com/repos/${repo}/commits`;
// 发起请求获取commit,并吐出的一段逻辑
const resp = await fetch(url, {
headers: {'User-Agent': 'Our Script'} // github需要
})
const body = await resp.json() // boyd的格式:CommitType[]
for(let commit of body) {
yield commit // 一个接一个的对外吐出commit
}
}
// 上面代码只是处理了初始第一页,我们需要根据响应头的下页地址,再自动去获取,因此我们更新url,如果新的url存在,说明还有下一页数据
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while(url) {
const resp = await fetch(url, {...}}
const body = await resp.json()
let nextPage = resp.headers.get('Link').match(/<(.*?)>; rel="next"/)
nextPage = nexPage?.[1] // 直到返回的没有Link
url = nextPage
for(let commit of body) {
yield commit
}
}
}
// 调用
(async () => {
let count = 0;
for await (const commit of fetchCommits('xxx)) {
console.log(commit.auther.login)
if(++count == 100) { // 数据太多,我们就Break下,测试嘛
break
}
}
})