generator
常规函数只会返回一个单一值(或者不返回任何值)。
而 generator 可以按需一个接一个地返回(“yield”)多个值。它们可与 iterable 完美配合使用,从而可以轻松地创建数据流。
generator 函数
要创建一个 generator,我们需要一个特殊的语法结构:function*
,即所谓的 “generator function”。
它看起来像这样:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
generator 函数与常规函数的行为不同。在此类函数被调用时,它不会运行其代码。而是返回一个被称为 “generator object” 的特殊对象,来管理执行流程。
我们来看一个例子:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence();
alert(generator); // [object Generator]
到目前为止,上面这段代码中的 函数体 代码还没有开始执行:
一个 generator 的主要方法就是 next()
。当被调用时(译注:指 next()
方法),它会恢复上图所示的运行,执行直到最近的 yield <value>
语句(value
可以被省略,默认为 undefined
)。然后函数执行暂停,并将产出的(yielded)值返回到外部代码。
next()
的结果始终是一个具有两个属性的对象:
value
: 产出的(yielded)的值。done
: 如果 generator 函数已执行完成则为true
,否则为false
。
例如,我们可以创建一个 generator 并获取其第一个产出的(yielded)值:
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
的值:
let two = generator.next();
alert(JSON.stringify(two)); // {value: 2, done: false}
如果我们第三次调用 generator.next()
,代码将会执行到 return
语句,此时就完成这个函数的执行:
let three = generator.next();
alert(JSON.stringify(three)); // {value: 3, done: true}
现在 generator 执行完成。我们通过 done:true
可以看出来这一点,并且将 value:3
处理为最终结果。
再对 generator.next()
进行新的调用不再有任何意义。如果我们这样做,它将返回相同的对象:{done: true}
。
function* f(…)
或function *f(…)
?
这两种语法都是对的。
但是通常更倾向于第一种语法,因为星号*
表示它是一个 generator 函数,它描述的是函数种类而不是名称,因此*
应该和function
关键字紧贴一起。
generator 是可迭代的
当你看到 next()
方法,或许你已经猜到了 generator 是 可迭代(iterable)的。
我们可以使用 for..of
循环遍历它所有的值:
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1,然后是 2
}
for..of
写法是不是看起来比 .next().value
优雅多了?
……但是请注意:上面这个例子会先显示 1
,然后是 2
,然后就没了。它不会显示 3
!
这是因为当 done: true
时,for..of
循环会忽略最后一个 value
。因此,如果我们想要通过 for..of
循环显示所有的结果,我们必须使用 yield
返回它们:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
alert(value); // 1,然后是 2,然后是 3
}
因为 generator 是可迭代的,我们可以使用 iterator 的所有相关功能,例如:spread 语法 ...
:
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let sequence = [0, ...generateSequence()];
alert(sequence); // 0, 1, 2, 3
在上面这段代码中,...generateSequence()
将可迭代的 generator 对象转换为了一个数组(关于 spread 语法的更多细节请见 Rest 参数与 Spread 语法)。
使用 generator 进行迭代
在前面的 Iterable object(可迭代对象) 一章中,我们创建了一个可迭代的 range
对象,它返回 from..to
的值。
现在,我们回忆一下代码:
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
,但紧凑得多:
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
之所以代码正常工作,是因为 range[Symbol.iterator]()
现在返回一个 generator,而 generator 方法正是 for..of
所期望的:
- 它具有
.next()
方法 - 它以
{value: ..., done: true/false}
的形式返回值
当然,这不是巧合。generator 被添加到 JavaScript 语言中是有对 iterator 的考量的,以便更容易地实现 iterator。
带有 generator 的变体比原来的range
迭代代码简洁得多,并且保持了相同的功能。
generator 可以永远产出(yield)值
在上面的示例中,我们生成了有限序列,但是我们也可以创建一个生成无限序列的 generator,它可以一直产出(yield)值。例如,无序的伪随机数序列。
这种情况下肯定需要在 generator 的for..of
循环中添加一个break
(或者return
)。否则循环将永远重复下去并挂起。
generator 组合
generator 组合(composition)是 generator 的一个特殊功能,它允许透明地(transparently)将 generator 彼此“嵌入(embed)”到一起。
例如,我们有一个生成数字序列的函数:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
现在,我们想重用它来生成一个更复杂的序列:
- 首先是数字
0..9
(字符代码为 48…57), - 接下来是大写字母
A..Z
(字符代码为 65…90) - 接下来是小写字母
a...z
(字符代码为 97…122)
我们可以对这个序列进行应用,例如,我们可以从这个序列中选择字符来创建密码(也可以添加语法字符),但让我们先生成它。
在常规函数中,要合并其他多个函数的结果,我们需要调用它们,存储它们的结果,最后再将它们合并到一起。
对于 generator 而言,我们可以使用yield*
这个特殊的语法来将一个 generator “嵌入”(组合)到另一个 generator 中:
组合的 generator 的例子:
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。这个术语意味着 yield* gen
在 generator gen
上进行迭代,并将其产出(yield)的值透明地(transparently)转发到外部。就好像这些值就是由外部的 generator yield 的一样。
执行结果与我们内联嵌套 generator 中的代码获得的结果相同:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) yield i;
}
function* generateAlphaNum() {
// yield* generateSequence(48, 57);
for (let i = 48; i <= 57; i++) yield i;
// yield* generateSequence(65, 90);
for (let i = 65; i <= 90; i++) yield i;
// yield* generateSequence(97, 122);
for (let i = 97; i <= 122; i++) yield i;
}
let str = '';
for(let code of generateAlphaNum()) {
str += String.fromCharCode(code);
}
alert(str); // 0..9A..Za..z
generator 组合(composition)是将一个 generator 流插入到另一个 generator 流的自然的方式。它不需要使用额外的内存来存储中间结果。
yield 是一条双向路
目前看来,generator 和可迭代对象类似,都具有用来生成值的特殊语法。但实际上,generator 更加强大且灵活。
这是因为 yield
是一条双向路(two-way street):它不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内。
调用 generator.next(arg)
,我们就能将参数 arg
传递到 generator 内部。这个 arg
参数会变成 yield
的结果。
我们来看一个例子:
function* gen() {
// 向外部代码传递一个问题并等待答案
let result = yield "2 + 2 = ?"; // (*)
alert(result);
}
let generator = gen();
let question = generator.next().value; // <-- yield 返回的 value
generator.next(4); // --> 将结果传递到 generator 中
- 第一次调用
generator.next()
应该是不带参数的(如果带参数,那么该参数会被忽略)。它开始执行并返回第一个yield "2 + 2 = ?"
的结果。此时,generator 执行暂停,而停留在(*)
行上。 - 然后,正如上面图片中显示的那样,
yield
的结果进入调用代码中的question
变量。 - 在
generator.next(4)
,generator 恢复执行,并获得了4
作为结果:let result = 4
。
请注意,外部代码不必立即调用next(4)
。外部代码可能需要一些时间。这没问题:generator 将等待它。
例如:
// 一段时间后恢复 generator
setTimeout(() => generator.next(4), 1000);
我们可以看到,与常规函数不同,generator 和调用 generator 的代码可以通过在 next/yield
中传递值来交换结果。
为了讲得更浅显易懂,我们来看另一个例子,其中包含了许多调用:
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
- 第一个
.next()
启动了 generator 的执行……执行到达第一个yield
。 - 结果被返回到外部代码中。
- 第二个
.next(4)
将4
作为第一个yield
的结果传递回 generator 并恢复 generator 的执行。 - ……执行到达第二个
yield
,它变成了 generator 调用的结果。 - 第三个
next(9)
将9
作为第二个yield
的结果传入 generator 并恢复 generator 的执行,执行现在到达了函数的最底部,所以返回done: true
。
这个过程就像“乒乓球”游戏。每个next(value)
(除了第一个)传递一个值到 generator 中,该值变成了当前yield
的结果,然后获取下一个yield
的结果。
generator.throw
正如我们在上面的例子中观察到的那样,外部代码可能会将一个值传递到 generator,作为 yield
的结果。
……但是它也可以在那里发起(抛出)一个 error。这很自然,因为 error 本身也是一种结果。
要向 yield
传递一个 error,我们应该调用 generator.throw(err)
。在这种情况下,err
将被抛到对应的 yield
所在的那一行。
例如,"2 + 2?"
的 yield 导致了一个 error:
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)
在 (2)
行引入到 generator 的 error 导致了在 (1)
行中的 yield
出现了一个异常。在上面这个例子中,try..catch
捕获并显示了这个 error。
如果我们没有捕获它,那么就会像其他的异常一样,它将从 generator “掉出”到调用代码中。
调用代码的当前行是 generator.throw
所在的那一行,标记为 (2)
。所以我们可以在这里捕获它,就像这样:
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
}
如果我们没有在那里捕获这个 error,那么,通常,它会掉入外部调用代码(如果有),如果在外部也没有被捕获,则会杀死脚本。
generator.return
generator.return(value)
完成 generator 的执行并返回给定的 value
。
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 上再次使用 generator.return()
,它将再次返回该值(MDN)。
通常我们不使用它,因为大多数时候我们想要获取所有的返回值,但是当我们想要在特定条件下停止 generator 时它会很有用。
异步迭代和 generator
异步迭代允许我们对按需通过异步请求而得到的数据进行迭代。例如,我们通过网络分段(chunk-by-chunk)下载数据时。异步生成器(generator)使这一步骤更加方便。
首先,让我们来看一个简单的示例以掌握语法,然后再看一个实际用例。
回顾可迭代对象
让我们回顾一下可迭代对象的相关内容。
假设我们有一个对象,例如下面的 range
:
let range = {
from: 1,
to: 5
};
我们想对它使用 for..of
循环,例如 for(value of range)
,来获取从 1
到 5
的值。
换句话说,我们想向对象 range
添加 迭代能力。
这可以通过使用一个名为 Symbol.iterator
的特殊方法来实现:
- 当循环开始时,该方法被
for..of
结构调用,并且它应该返回一个带有next
方法的对象。 - 对于每次迭代,都会为下一个值调用
next()
方法。 next()
方法应该以{done: true/false, value:<loop value>}
的格式返回一个值,其中done:true
表示循环结束。
这是可迭代的range
的一个实现:
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)
循环来迭代这样的对象。- 注意关键字
await
。
作为开始的示例,让我们创建一个可迭代的range
对象,与前面的那个类似,不过现在它将异步地每秒返回一个值。
我们需要做的就是对上面代码中的部分代码进行替换:
- 注意关键字
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 类似:
- 为了使一个对象可以异步迭代,它必须具有方法
Symbol.asyncIterator
(1)
。 - 这个方法必须返回一个带有
next()
方法的对象,next()
方法会返回一个 promise(2)
。 - 这个
next()
方法可以不是async
的,它可以是一个返回值是一个promise
的常规的方法,但是使用async
关键字可以允许我们在方法内部使用await
,所以会更加方便。这里我们只是用于延迟 1 秒的操作(3)
。 - 我们使用
for await(let value of range)
(4)
来进行迭代,也就是在for
后面添加await
。它会调用一次range[Symbol.asyncIterator]()
方法一次,然后调用它的next()
方法获取值。
这是一个对比 Iterator 和异步 iterator 之间差异的表格:
Iterator | 异步 iterator | |
---|---|---|
提供 iterator 的对象方法 | Symbol.iterator |
Symbol.asyncIterator |
next() 返回的值是 |
任意值 | Promise |
要进行循环,使用 | for..of |
for await..of |
Spread 语法
...
无法异步工作
需要常规的同步 iterator 的功能,无法与异步 iterator 一起使用。
例如,spread 语法无法工作:
alert( [...range] ); // Error, no Symbol.iterator
这很正常,因为它期望找到
Symbol.iterator
,而不是Symbol.asyncIterator
。
for..of
的情况和这个一样:没有await
关键字时,则期望找到的是Symbol.iterator
。
回顾 generator
现在,让我们回顾一下 generator,它使我们能够写出更短的迭代代码。在大多数时候,当我们想要创建一个可迭代对象时,我们会使用 generator。
简单起见,这里省略了一些解释,即 generator 是“生成(yield)值的函数”。关于此的详细说明请见 generator 一章。
Generator 是标有 function*
(注意星号)的函数,它使用 yield
来生成值,并且我们可以使用 for..of
循环来遍历它们。
下面这例子生成了从 start
到 end
的一系列值:
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
。
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
return <带有 next 方法的对象,以使对象 range 可迭代>
}
}
对于 Symbol.iterator
来说,一个通常的做法是返回一个 generator,这样可以使代码更短,如下所示:
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 中,我们无法使用 await
。所有的值都必须按照 for..of
构造的要求同步地出现。
如果我们想要异步地生成值该怎么办?例如,对于来自网络请求的值。
让我们再回到异步 generator,来使这个需求成为可能。
异步 generator(finally)
对于大多数的实际应用程序,当我们想创建一个异步生成一系列值的对象时,我们都可以使用异步 generator。
语法很简单:在 function*
前面加上 async
。这即可使 generator 变为异步的。
然后使用 for await (...)
来遍历它,像这样:
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 之间有延迟)
}
})();
因为此 generator 是异步的,所以我们可以在其内部使用 await
,依赖于 promise
,执行网络请求等任务。
引擎盖下的差异
如果你还记得我们在前面章节中所讲的关于 generator 的细节知识,那你应该知道,从技术上讲,异步 generator 和常规的 generator 在内部是有区别的。
对于异步 generator,generator.next()
方法是异步的,它返回 promise。
在一个常规的 generator 中,我们使用result = generator.next()
来获得值。但在一个异步 generator 中,我们应该添加await
关键字,像这样:
result = await generator.next(); // result = {value: ..., done: true/false}
这就是为什么异步 generator 可以与
for await...of
一起工作。
异步的可迭代对象 range
常规的 generator 可用作 Symbol.iterator
以使迭代代码更短。
与之类似,异步 generator 可用作 Symbol.asyncIterator
来实现异步迭代。
例如,我们可以通过将同步的 Symbol.iterator
替换为异步的 Symbol.asyncIterator
,来使对象 range
异步地生成值,每秒生成一个:
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
}
})();
现在,value 之间的延迟为 1 秒。
请注意:
从技术上讲,我们可以把Symbol.iterator
和Symbol.asyncIterator
都添加到对象中,因此它既可以是同步的(for..of
)也可以是异步的(for await..of
)可迭代对象。
但是实际上,这将是一件很奇怪的事情。
实际的例子:分页的数据
到目前为止,我们已经了解了一些基本示例,以加深理解。现在,我们来看一个实际的用例。
目前,有很多在线服务都是发送的分页的数据(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
异步迭代。
因此,其用法将如下所示:
for await (let commit of fetchCommits("username/repository")) {
// 处理 commit
}
通过异步 generator,我们可以轻松实现上面所描述的函数,如下所示:
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;
}
}
}
关于其工作原理的进一步解释:
- 我们使用浏览器的 fetch 方法来下载 commit。
- 初始 URL 是
https://api.github.com/repos/<repo>/commits
,并且下一页的 URL 将在响应的Link
header 中。 fetch
方法允许我们提供授权和其他 header,如果需要 —— 这里 GitHub 需要的是User-Agent
。
- 初始 URL 是
- commit 被以 JSON 的格式返回。
- 我们应该从响应(response)的
Link
header 中获取前往下一页的 URL。它有一个特殊的格式,所以我们对它使用正则表达式(我们将在 正则表达式 一章中学习它)。- 前往下一页的 URL 看起来可能就像这样
https://api.github.com/repositories/93253246/commits?page=2
。这是由 GitHub 自己生成的。
- 前往下一页的 URL 看起来可能就像这样
- 然后,我们将接收到的所有 commit 一个一个地 yield 出来,当所有 commit 都 yield 完成时,将触发下一个
while(url)
迭代,并发出下一个请求。
这是一个使用示例(在控制台中显示 commit 的作者)
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) { // 让我们在获取了 100 个 commit 时停止
break;
}
}
})();
// 注意:如果你在外部沙箱中运行它,你需要把上面的 fetchCommits 函数粘贴到这儿。
这就是我们想要的。
从外部看不到分页请求(paginated requests)的内部机制。对我们来说,它只是一个返回 commit 的异步 generator。