Generator

常规函数只返回一个单一值,或不返回任何值。
而生成器函数可以按需的一个接一个返回多个值,通过yield向外透出值,且可以与 iterable完美结合

语法

  1. function* gen() {
  2. yield 1;
  3. yield 2;
  4. return 3
  5. }
  6. // 调用该函数,不会运行其中的代码,而是返回一个具有next函数的对象,通过next来运行函数中的代码
  7. // next调用产出的结果是一个对象{value: xx, done: boolean}
  8. let g = gen() // 持有generator对象,并未运行
  9. let one = g.next() // 第一步yield 1, 返回 {value: 1, done: false}
  10. let two = g.next() // 第二步yield 2, 返回 {value: 2, done: false}
  11. let three = g.next() // 第三步yield 3, 返回 {value: 3, done: true}

可迭代

生成器调用后返回的是一个可迭代的对象,所以我们可以使用for of来遍历(自动执行)

注意:凡是可迭代的对象,都可以用for of来遍历。

  1. function* gen() {
  2. yield 1;
  3. yield 2;
  4. return 3
  5. }
  6. for(let v of gen()) {
  7. console.log(v) // 1, 2 注意!没有3,跟上面手动运行next不同
  8. }

没有3是因为for of会忽略done: true时的最后一个value,我们可以将return改为yield,这样就可以拿到3。
因为可迭代,我们就可以使用Spread语法。

  1. function *gen() {
  2. yield 1
  3. yield 2
  4. yield 3
  5. }
  6. let arr = [0, ...gen()] // 0, 1, 2, 3 同样,将return改为yield才能有3

使用generator进行迭代

  1. // 复习下:可以被for of的对象,称为可迭代对象
  2. // 我们可以通过Symbol.iterator来实现一个对象的迭代函数,for of会去调用它
  3. let range = {
  4. from: 1,
  5. to: 5,
  6. [Symbol.iterator]() {
  7. return {
  8. c: this.from,
  9. l: this.to,
  10. next() {
  11. if(this.c <= this.l) {
  12. // 这里this.c++ 先赋值给value了,再+
  13. return {value: this.c++, done: false}
  14. } else {
  15. return {done: true}
  16. }
  17. }
  18. }
  19. }
  20. }
  21. [...range] // [1,2,3,4,5]

改写成generator

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. [Symbol.iterator]* () {
  5. for(let v = this.from; v <= this.to; v ++) {
  6. yield v
  7. }
  8. }
  9. }
  10. [...range] // [1,2,3,4,5] 比上面自己实现的具备next的iterator更简短

generator组合

一个特殊功能,允许多个生成器透明的组合起来

  1. // 一个生成数字的生成器,给定起始数字
  2. function *genNum(start, end) {
  3. for(let i = start; i <= end; i++) {
  4. yield i
  5. }
  6. }
  7. // 我们可以利用上面来生成code码,然后code码转成字符
  8. function *genPassword() {
  9. yield *genNum(48, 57)
  10. yield *genNum(65, 90)
  11. yield *genNum(97, 122)
  12. }
  13. let str = ''
  14. for(let code of genPassword()) {
  15. // 可以看出genNum将它生成的所有数字,都透明的吐给了genPassword生成器
  16. str+=String.fromCharCode(code)
  17. }
  1. // 上面类似
  2. function *genPassword() {
  3. for(let i = 48; i <= 57; i++) {
  4. yield i
  5. }
  6. for(let i = 65; i <= 90; i++) {
  7. yield i
  8. }
  9. for(let i = 97; i <= 122; i++) {
  10. yield i
  11. }
  12. }
  13. let str = ''
  14. for(let num of genPassword()) {
  15. str += String.fromCharCode(num)
  16. }

这就体现了上面说的 透明 2个字。

yield 是一条双向路

即可以向外吐出值,也可以接收外部的值。

  1. let 从外部接收的返回值作为yield调用的返回值 = yield 吐到外面的值
  2. // yield question 对外扔出question
  3. // generator.next(4) 对question作出回答,扔进generator中,作为 从外部接收的返回值作为yield调用的返回值
  4. function *gen() {
  5. let res = yield '2+2=?'
  6. alert(res)
  7. }
  8. let generator = gen()
  9. // 这里,第一个next(入参),入参会被忽略掉。
  10. generator.next(4) {value: '2+2=?', done: false}
  11. generator.next(4) // 弹出4

注意:生成器的第一个next(arg)调用,如果传入了arg,会忽略掉

  1. // 一个复杂点的例子
  2. function* gen() {
  3. let ask1 = yield "2 + 2 = ?";
  4. alert(ask1); // 4
  5. let ask2 = yield "3 * 3 = ?"
  6. alert(ask2); // 9
  7. }
  8. let g = gen();
  9. g.next() // {value: '2+2=?', done: false}
  10. g.next(4) // 传递给当前yield的返回值,赋值给ask1,弹出4 {value: '3*3=?', done: false}
  11. g.next(9) // 9赋值给ask2,弹出9,{value: undefined, done: true}

generator.throw

外部代码可能也会抛出错误,错误error也是一种值,作为yield的结果,可以使用。
此时需要用到throw这个api

  1. function* gen() {
  2. try {
  3. let result = yield "2 + 2 = ?"; // (1)
  4. alert("The execution does not reach here, because the exception is thrown above");
  5. } catch(e) {
  6. // 这里被捕获到
  7. alert(e); // 显示这个 error
  8. }
  9. }
  10. let generator = gen();
  11. let question = generator.next().value;
  12. generator.throw(new Error('error啦')) // 这里抛出的错误

如果你遗漏了错误捕获,错误将从generator中掉到调用地的代码中

  1. // 你也可以在调用地捕获
  2. function* generate() {
  3. let result = yield "2 + 2 = ?"; // 这行出现 error
  4. }
  5. let generator = generate();
  6. let question = generator.next().value;
  7. try {
  8. generator.throw(new Error("The answer is not found in my database"));
  9. } catch(e) {
  10. alert(e); // 显示这个 error
  11. }

总结

  • 通过function *f() {}创建的生成器函数
  • 生成器函数调用时,返回一个可迭代的对象,具有next方法来控制运行流程。
    • 因为是可迭代的,因此可以配合Spread语法使用。
  • 使用yield我们可以向外部吐出值,使用next(arg)我们可以向生成器内部传递值,传递给当前yield,作为其返回值。
    1. function *f() {
    2. const name = yield 'your name?'
    3. console.log(name)
    4. }
    5. // 运行生成器得到可迭代对象
    6. const generator = f()
    7. generator.next() // 正式开始运行生成器内部代码,第一个yield被执行,向外吐出 {value: 'your name?', done: false}
    8. 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. 上面的迭代都是同步的逻辑,我们来看下异步的。
  2. 1. 异步我们使用`Symbol.asyncIterator`取代`Symbol.iterator`
  3. 1. next函数需要返回一个promise值,这个我们可以使用async,天然返回一个promise
  4. 1. 使用`for await (let item of iterable)`来循环异步可迭代对象
  5. 我们来试下异步可迭代对象
  6. ```javascript
  7. const delay = function(t) {
  8. return new Promise(resolve => setTimeout(() => {
  9. resolve(true)}, t))
  10. }
  11. let range = {
  12. from: 1,
  13. to: 5,
  14. [Symbol.asyncIterator]() {
  15. return {
  16. current: this.from,
  17. last: this.to,
  18. // 第二点
  19. async next() {
  20. await delay(1000)
  21. if(this.current <= this.last) {
  22. // 返回值会被包装成resolved promise
  23. return {done: false, value: this.current++}
  24. } else {
  25. return {done: true}
  26. }
  27. }
  28. }
  29. }
  30. }
  31. // 开始迭代它
  32. async function t () => {
  33. for await (let v of range) {console.log(v)}
  34. }
  35. t() // 间隔1秒打印1 2 3 4 5

与同步的可迭代对象不同,不能使用Spread语法,因为其只会去找[Symbol.iterator],如果你不用for await一样不行。

回顾下generator

generator函数可以生成iterator可迭代对象,所以我们可以用for of来循环

  1. function *gen(start, end) {
  2. for(let i = start; i <= end; i++) {
  3. yield i
  4. }
  5. }
  6. for(let v of gen(1, 5)) {console.log(v)} // 1, 2, 3, 4, 5
  7. // 简短的写法来让一个对象可迭代
  8. let range = {
  9. from: 1,
  10. to: 5,
  11. *[Symbol.iterator]() {
  12. for(let v = this.from; v <= this.to; v++) {
  13. yield v
  14. }
  15. }
  16. }
  17. for(let v of range) {console.log(v)} // 1,2,3,4,5

我们来看看异步的吧,加上async就可以使其成为异步生成器

  1. const delay = function(t) {
  2. return new Promise(resolve => setTimeout(() => {
  3. resolve(true)}, t))
  4. }
  5. async function *gen(start, end) {
  6. for(let i = start; i <= end; i++) {
  7. await delay(1000)
  8. yield i
  9. }
  10. }
  11. // 使用for await,所以要包在async里
  12. (async () => {
  13. let generator = generateSequence(1, 5);
  14. for await (let value of generator) {
  15. alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟)
  16. }
  17. // for await 相当于在迭代器内部这样循环调用await next()
  18. })();

将对象变成异步可迭代对象

  1. let range = {
  2. from: 1,
  3. to: 5,
  4. // async包装成异步逻辑
  5. // *使用生成器函数
  6. async *[Symbol.asyncIterator]() {
  7. for(let v = this.from; v <= this.to; v++) {
  8. // 延迟
  9. await delay(1000)
  10. // 吐值
  11. yield value
  12. }
  13. }
  14. }
  15. (async () => {
  16. for await (let value of range) {
  17. alert(value); // 1,然后 2,然后 3,然后 4,然后 5
  18. }
  19. })();

一个实用的例子

翻页数据通常会去点击它才进行翻页,这里我们实现一个函数,可以自动拉取github上的commit(分页)。

用法

  1. for await (let commit of fetchCommits('someone/repo)) {
  2. // 处理commit
  3. }

通过异步迭代来获取

github的这个接口调用后,会将下一页的数据查询链接放在响应头Link里,具有一定格式,我们可以在调取下一页数据时使用

  1. async function* fetchCommits(repo) {
  2. // 构造初始仓库commit接口地址
  3. let url = `https://api.github.com/repos/${repo}/commits`;
  4. // 发起请求获取commit,并吐出的一段逻辑
  5. const resp = await fetch(url, {
  6. headers: {'User-Agent': 'Our Script'} // github需要
  7. })
  8. const body = await resp.json() // boyd的格式:CommitType[]
  9. for(let commit of body) {
  10. yield commit // 一个接一个的对外吐出commit
  11. }
  12. }
  1. // 上面代码只是处理了初始第一页,我们需要根据响应头的下页地址,再自动去获取,因此我们更新url,如果新的url存在,说明还有下一页数据
  2. async function* fetchCommits(repo) {
  3. let url = `https://api.github.com/repos/${repo}/commits`;
  4. while(url) {
  5. const resp = await fetch(url, {...}}
  6. const body = await resp.json()
  7. let nextPage = resp.headers.get('Link').match(/<(.*?)>; rel="next"/)
  8. nextPage = nexPage?.[1] // 直到返回的没有Link
  9. url = nextPage
  10. for(let commit of body) {
  11. yield commit
  12. }
  13. }
  14. }
  15. // 调用
  16. (async () => {
  17. let count = 0;
  18. for await (const commit of fetchCommits('xxx)) {
  19. console.log(commit.auther.login)
  20. if(++count == 100) { // 数据太多,我们就Break下,测试嘛
  21. break
  22. }
  23. }
  24. })

总结

同步/异步可迭代对象

  1. 同步迭代对象使用[Symbol.iterator]for ofSpread语法都会去查找它。而异步可迭代对象使用[Symbol.asyncIterator],需要使用for await来进行循环,Spread不适用异步。
  2. 可迭代对象的next()方法返回值不同,异步返回的是包装了{value:xx, done: boolean}的promise值。

    同步/异步生成器

  3. 异步生成器,我们使用async包装一下

  4. next()返回的需要是一个promise