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 1yield 2yield 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); // 4let 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,天然返回一个promise1. 使用`for await (let item of iterable)`来循环异步可迭代对象我们来试下异步可迭代对象```javascriptconst 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 promisereturn {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] // 直到返回的没有Linkurl = nextPagefor(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}}})
