generator
概述
- 常规函数只会返回一个单一值(或者不返回任何值)
- 而
generator可以按需一个接一个地返回(“yield”)多个值- 它们可与
iterable完美配合使用,从而可以轻松地创建数据流
- 它们可与
generator 函数
- 要创建一个
generator,我们需要一个特殊的语法结构:function*,即所谓的 “generator function”
|
1 2 3 4 5 |
function* generateSequence() { yield 1; yield 2; return 3; } |
generator函数与常规函数的行为不同- 在此类函数被调用时,它不会运行其代码
- 而是返回一个被称为
“generator object”的特殊对象,来管理执行流程
|
1 2 3 4 5 6 7 8 9 |
function* generateSequence() { yield 1; yield 2; return 3; } // "generator function" 创建了一个 "generator object" let generator = generateSequence(); alert(generator); // [object Generator] |
-
到目前为止,上面这段代码中的 函数体 代码还没有开始执行:
- 一个
generator的主要方法就是next() - 当被调用时,它会恢复运行,执行直到最近的
yield <value>语句(value可以被省略,默认为undefined) - 然后函数执行暂停,并将产出的(
yielded)值返回到外部代码
- 一个
-
next()的结果始终是一个具有两个属性的对象:value: 产出的(yielded)的值done: 如果 generator 函数已执行完成则为true,否则为false
-
例如,我们可以创建一个
generator并获取其第一个产出的(yielded)值:- 截至目前,我们只获得了第一个值,现在函数执行处在第二行:
|
1 2 3 4 5 6 7 8 9 10 11 |
function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); let one = generator.next(); alert(JSON.stringify(one)); // {value: 1, done: false} |
- 再次调用
generator.next()。代码恢复执行并返回下一个yield的值:- 如果我们第三次调用
generator.next(),代码将会执行到return语句,此时就完成这个函数的执行:
- 如果我们第三次调用
|
1 2 3 |
let two = generator.next(); alert(JSON.stringify(two)); // {value: 2, done: false} |
|
1 2 3 |
let three = generator.next(); alert(JSON.stringify(three)); // {value: 3, done: true} |
- 现在
generator执行完成。我们通过done:true可以看出来这一点,并且将value:3处理为最终结果- 再对
generator.next()进行新的调用不再有任何意义。如果我们这样做,它将返回相同的对象:{done: true}
- 再对
generator 是可迭代的
- 可以使用
for..of循环遍历它所有的值:- 这个例子会先显示
1,然后是2,然后就没了。它不会显示3
- 这个例子会先显示
|
1 2 3 4 5 6 7 8 9 10 11 |
function* generateSequence() { yield 1; yield 2; return 3; } let generator = generateSequence(); for(let value of generator) { alert(value); // 1,然后是 2 } |
- 这是因为当
done: true时,for..of循环会忽略最后一个value- 因此,如果我们想要通过
for..of循环显示所有的结果,我们必须使用yield返回它们:
- 因此,如果我们想要通过
|
1 2 3 4 5 6 7 8 9 10 11 |
function* generateSequence() { yield 1; yield 2; yield 3; } let generator = generateSequence(); for(let value of generator) { alert(value); // 1,然后是 2,然后是 3 } |
- 因为
generator是可迭代的,我们可以使用iterator的所有相关功能- 例如:spread 语法
...: - 这段代码中,
...generateSequence()将可迭代的generator对象转换为了一个数组
- 例如:spread 语法
|
1 2 3 4 5 6 7 8 9 |
function* generateSequence() { yield 1; yield 2; yield 3; } let sequence = [0, ...generateSequence()]; alert(sequence); // 0, 1, 2, 3 |
使用 generator 进行迭代
- 下面代码创建了一个可迭代的
range对象,它返回from..to的值
|
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 |
let range = { from: 1, to: 5, // for..of range 在一开始就调用一次这个方法 [Symbol.iterator]() { // ...它返回 iterator object: // 后续的操作中,for..of 将只针对这个对象,并使用 next() 向它请求下一个值 return { current: this.from, last: this.to, // for..of 循环在每次迭代时都会调用 next() next() { // 它应该以对象 {done:.., value :...} 的形式返回值 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; // 迭代整个 range 对象,返回从 `range.from` 到 `range.to` 范围的所有数字 alert([...range]); // 1,2,3,4,5 |
- 我们可以通过提供一个
generator函数作为Symbol.iterator,来使用generator进行迭代:- 之所以代码正常工作,是因为
range[Symbol.iterator]()现在返回一个generator,而generator方法正是for..of所期望的: - 它具有
.next()方法 - 它以
{value: ..., done: true/false}的形式返回值 - 这不是巧合。
generator被添加到JavaScript语言中是有对iterator的考量的,以便更容易地实现iterator - 带有
generator的变体比原来的range迭代代码简洁得多,并且保持了相同的功能
- 之所以代码正常工作,是因为
|
1 2 3 4 5 6 7 8 9 10 11 12 |
let range = { from: 1, to: 5, *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式 for(let value = this.from; value <= this.to; value++) { yield value; } } }; alert( [...range] ); // 1,2,3,4,5 |
注意
generator可以永远产出(yield)值- 上面的示例中,我们生成了有限序列,但是我们也可以创建一个生成无限序列的
generator,它可以一直产出(yield)值 - 例如,无序的伪随机数序列
- 这种情况下肯定需要在
generator的for..of循环中添加一个break(或者return)。否则循环将永远重复下去并挂起
- 上面的示例中,我们生成了有限序列,但是我们也可以创建一个生成无限序列的
generator 组合
generator组合(composition)是generator的一个特殊功能,它允许透明地(transparently)将generator彼此“嵌入(embed)”到一起- 例如,我们有一个生成数字序列的函数:
|
1 2 3 |
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } |
- 现在,我们想重用它来生成一个更复杂的序列:
- 对于
generator而言,我们可以使用yield*这个特殊的语法来将一个generator“嵌入”(组合)到另一个generator中: yield*指令将执行 委托 给另一个generator- 这个术语意味着
yield* gen在generatorgen上进行迭代,并将其产出(yield)的值透明地(transparently)转发到外部。就好像这些值就是由外部的generatoryield的一样
- 对于
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
function* generateSequence(start, end) { for (let i = start; i <= end; i++) yield i; } function* generatePasswordCodes() { // 0..9 yield* generateSequence(48, 57); // A..Z yield* generateSequence(65, 90); // a..z yield* generateSequence(97, 122); } let str = ''; for(let code of generatePasswordCodes()) { str += String.fromCharCode(code); } alert(str); // 0..9A..Za..z |
“yield” 是一条双向路
- 目前看来,
generator和可迭代对象类似,都具有用来生成值的特殊语法- 但实际上,
generator更加强大且灵活
- 但实际上,
- 这是因为
yield是一条双向路(two-way street):它不仅可以向外返回结果,而且还可以将外部的值传递到generator内- 调用
generator.next(arg),我们就能将参数arg传递到generator内部 - 这个
arg参数会变成yield的结果 - 第一次调用
generator.next()应该是不带参数的(如果带参数,那么该参数会被忽略) - 它开始执行并返回第一个
yield "2 + 2 = ?"的结果。此时,generator执行暂停,而停留在(*)行上 - 在
generator.next(4),generator恢复执行,并获得了4作为结果:let result = 4 - 请注意,外部代码不必立即调用
next(4)。外部代码可能需要一些时间。这没问题:generator将等待它
- 调用
|
1 2 3 4 5 6 7 8 9 10 11 12 |
function* gen() { // 向外部代码传递一个问题并等待答案 let result = yield "2 + 2 = ?"; // (*) alert(result); } let generator = gen(); let question = generator.next().value; // <-- yield 返回的 value generator.next(4); // --> 将结果传递到 generator 中 |
|
1 2 |
// 一段时间后恢复 generator setTimeout(() => generator.next(4), 1000); |
- 可以看到,与常规函数不同,
generator和调用generator的代码可以通过在next/yield中传递值来交换结果- 第一个
.next()启动了generator的执行……执行到达第一个yield - 结果被返回到外部代码中
- 第二个
.next(4)将4作为第一个yield的结果传递回generator并恢复generator的执行 - 执行到达第二个
yield,它变成了generator调用的结果 - 第三个
next(9)将9作为第二个yield的结果传入generator并恢复generator的执行,执行现在到达了函数的最底部,所以返回done: true
- 第一个
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
function* gen() { let ask1 = yield "2 + 2 = ?"; alert(ask1); // 4 let ask2 = yield "3 * 3 = ?" alert(ask2); // 9 } let generator = gen(); alert( generator.next().value ); // "2 + 2 = ?" alert( generator.next(4).value ); // "3 * 3 = ?" alert( generator.next(9).done ); // true |
generator.throw
- 正如我们在上面的例子中观察到的那样,外部代码可能会将一个值传递到
generator,作为yield的结果- 但是它也可以在那里发起(抛出)一个
error。这很自然,因为error本身也是一种结果 - 要向
yield传递一个error,我们应该调用generator.throw(err)。在这种情况下,err将被抛到对应的yield所在的那一行 - 在
(2)行引入到generator的error导致了在(1)行中的yield出现了一个异常
- 但是它也可以在那里发起(抛出)一个
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
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("The answer is not found in my database")); // (2) |
- 如果我们没有捕获它,那么就会像其他的异常一样,它将从
generator“掉出”到调用代码中
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
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 } |
generator.return
generator.return(value)完成generator的执行并返回给定的value- 如果我们在已完成的
generator上再次使用generator.return(),它将再次返回该值 - 通常我们不使用它,因为大多数时候我们想要获取所有的返回值,但是当我们想要在特定条件下停止
generator时它会很有用
- 如果我们在已完成的
|
1 2 3 4 5 6 7 8 9 10 11 |
function* gen() { yield 1; yield 2; yield 3; } const g = gen(); g.next(); // { value: 1, done: false } g.return('foo'); // { value: "foo", done: true } g.next(); // { value: undefined, done: true } |
异步迭代和 generator
概述
- 异步迭代允许我们对按需通过异步请求而得到的数据进行迭代
- 例如,我们通过网络分段(
chunk-by-chunk)下载数据时。异步生成器(generator)使这一步骤更加方便
示例
- 假设我们有一个对象,例如下面的
range:- 我们想对它使用
for..of循环,例如for(value of range),来获取从1到5的值 - 换句话说,我们想向对象
range添加 迭代能力 - 这可以通过使用一个名为
Symbol.iterator的特殊方法来实现: - 当循环开始时,该方法被
for..of结构调用,并且它应该返回一个带有next方法的对象 - 对于每次迭代,都会为下一个值调用
next()方法 next()方法应该以{done: true/false, value:<loop value>}的格式返回一个值,其中done:true表示循环结束
- 我们想对它使用
|
1 2 3 4 |
let range = { from: 1, to: 5 }; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let range = { from: 1, to: 5, [Symbol.iterator]() { // 在 for..of 循环开始时被调用一次 return { current: this.from, last: this.to, next() { // 每次迭代时都会被调用,来获取下一个值 if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; for(let value of range) { alert(value); // 1,然后 2,然后 3,然后 4,然后 5 } |
异步可迭代对象
- 当值是以异步的形式出现时,例如在
setTimeout或者另一种延迟之后,就需要异步迭代- 最常见的场景是,对象需要发送一个网络请求以传递下一个值
- 要使对象异步迭代:
- 使用
Symbol.asyncIterator取代Symbol.iterator next()方法应该返回一个promise(带有下一个值,并且状态为fulfilled)
关键字async可以实现这一点,我们可以简单地使用async next()- 我们应该使用
for await (let item of iterable)循环来迭代这样的对象
- 使用
- 创建一个可迭代的
range对象,与前面的那个类似,不过现在它将异步地每秒返回一个值- 为了使一个对象可以异步迭代,它必须具有方法
Symbol.asyncIterator(1) - 这个方法必须返回一个带有
next()方法的对象,next()方法会返回一个promise(2) - 这个
next()方法可以不是async的,它可以是一个返回值是一个promise的常规的方法,但是使用async关键字可以允许我们在方法内部使用await,所以会更加方便 - 我们使用
for await(let value of range)(4)来进行迭代,也就是在for后面添加await
它会调用一次range[Symbol.asyncIterator]()方法一次,然后调用它的next()方法获取值
- 为了使一个对象可以异步迭代,它必须具有方法
|
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 28 29 30 31 |
let range = { from: 1, to: 5, [Symbol.asyncIterator]() { // (1) return { current: this.from, last: this.to, async next() { // (2) // 注意:我们可以在 async next 内部使用 "await" await new Promise(resolve => setTimeout(resolve, 1000)); // (3) if (this.current <= this.last) { return { done: false, value: this.current++ }; } else { return { done: true }; } } }; } }; (async () => { for await (let value of range) { // (4) alert(value); // 1,2,3,4,5 } })() |
注意
- 需要常规的同步
iterator的功能,无法与异步iterator一起使用- 例如,
spread语法无法工作: - 这很正常,因为它期望找到
Symbol.iterator,而不是Symbol.asyncIterator for..of的情况和这个一样:没有await关键字时,则期望找到的是Symbol.iterator
- 例如,
|
1 |
alert( [...range] ); // Error, no Symbol.iterator |
回顾 generator
Generator是标有function*(注意星号)的函数,它使用yield来生成值,并且我们可以使用for..of循环来遍历它们
|
1 2 3 4 5 6 7 8 9 |
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 } |
- 正如我们所知道的,要使一个对象可迭代,我们需要给它添加
Symbol.iterator
|
1 2 3 4 5 6 7 |
let range = { from: 1, to: 5, [Symbol.iterator]() { return <带有 next 方法的对象,以使对象 range 可迭代> } } |
- 对于
Symbol.iterator来说,一个通常的做法是返回一个generator,这样可以使代码更短,如下所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
let range = { from: 1, to: 5, *[Symbol.iterator]() { // [Symbol.iterator]: function*() 的一种简写 for(let value = this.from; value <= this.to; value++) { yield value; } } }; for(let value of range) { alert(value); // 1,然后 2,然后 3,然后 4,然后 5 } |
异步 generator (finally)
- 对于大多数的实际应用程序,当我们想创建一个异步生成一系列值的对象时,我们都可以使用异步
generator - 语法很简单:在
function*前面加上async。这即可使generator变为异步的 - 然后使用
for await (...)来遍历它,像这样: - 因为此
generator是异步的,所以我们可以在其内部使用await,依赖于promise,执行网络请求等任务
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
async function* generateSequence(start, end) { for (let i = start; i <= end; i++) { // 哇,可以使用 await 了! await new Promise(resolve => setTimeout(resolve, 1000)); yield i; } } (async () => { let generator = generateSequence(1, 5); for await (let value of generator) { alert(value); // 1,然后 2,然后 3,然后 4,然后 5(在每个 alert 之间有延迟) } })(); |
异步的可迭代对象 range
- 常规的
generator可用作Symbol.iterator以使迭代代码更短 - 与之类似,异步
generator可用作Symbol.asyncIterator来实现异步迭代- 例如,我们可以通过将同步的
Symbol.iterator替换为异步的Symbol.asyncIterator,来使对象range异步地生成值,每秒生成一个:
- 例如,我们可以通过将同步的
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
let range = { from: 1, to: 5, // 这一行等价于 [Symbol.asyncIterator]: async function*() { async *[Symbol.asyncIterator]() { for(let value = this.from; value <= this.to; value++) { // 在 value 之间暂停一会儿,等待一些东西 await new Promise(resolve => setTimeout(resolve, 1000)); yield value; } } }; (async () => { for await (let value of range) { alert(value); // 1,然后 2,然后 3,然后 4,然后 5 } })(); |
实际的例子:分页的数据
- 目前,有很多在线服务都是发送的分页的数据(
paginated data)- 例如,当我们需要一个用户列表时,一个请求只返回一个预设数量的用户(例如
100个用户)—— “一页”,并提供了指向下一页的URL
- 例如,当我们需要一个用户列表时,一个请求只返回一个预设数量的用户(例如
- 这种模式非常常见。不仅可用于获取用户列表,这种模式还可以用于任意东西
- 例如,
GitHub允许使用相同的分页提交(paginated fashion)的方式找回commit: - 我们应该以
https://api.github.com/repos/<repo>/commits格式创建进行fetch的网络请求 - 它返回一个包含
30条commit的JSON,并在返回的Linkheader中提供了指向下一页的链接 - 然后我们可以将该链接用于下一个请求,以获取更多
commit,以此类推
- 例如,
- 对于我们的代码,我们希望有一种更简单的获取
commit的方式- 让我们创建一个函数
fetchCommits(repo),用来在任何我们有需要的时候发出请求,来为我们获取commit - 并且,该函数能够关注到所有分页内容
- 对于我们来说,它将是一个简单的
for await..of异步迭代
- 让我们创建一个函数
|
1 2 3 |
for await (let commit of fetchCommits("username/repository")) { // 处理 commit } |
- 通过异步
generator,我们可以轻松实现上面所描述的函数,如下所示:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
async function* fetchCommits(repo) { let url = `https://api.github.com/repos/${repo}/commits`; while (url) { const response = await fetch(url, { // (1) headers: {'User-Agent': 'Our script'}, // github 需要任意的 user-agent header }); const body = await response.json(); // (2) 响应的是 JSON(array of commits) // (3) 前往下一页的 URL 在 header 中,提取它 let nextPage = response.headers.get('Link').match(/<(.*?)>; rel="next"/); nextPage = nextPage?.[1]; url = nextPage; for(let commit of body) { // (4) 一个接一个地 yield commit,直到最后一页 yield commit; } } } |
声明:本文为原创文章,版权归Aet所有,欢迎分享本文,转载请保留出处!