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
在generator
gen
上进行迭代,并将其产出(yield
)的值透明地(transparently
)转发到外部。就好像这些值就是由外部的generator
yield
的一样
- 对于
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
,并在返回的Link
header
中提供了指向下一页的链接 - 然后我们可以将该链接用于下一个请求,以获取更多
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所有,欢迎分享本文,转载请保留出处!
你可能也喜欢
- ♥ 【Javascript】第二部分05/14
- ♥ HTTP协议相关学习一03/22
- ♥ 【Javascript】应用一:图片,视频,编码相关04/29
- ♥ 【Javascript】对象引用复制05/18
- ♥ 【LeetCode-30 天 JavaScript 挑战】07/23
- ♥ cef:任务、IPC、网络相关04/30