生成器-高级迭代
常规函数只能返回一个值或一个对象,或者不返回值,但只要是返回,必然是一次返回全部数据。
然而生成器却允许按需一个一个的返回。
Generator 生成器
生成器函数
语法:
function* generateSequence() { // 方式一
yield value;
}
写成function *generateSequence // 方式二 也是可以的
但规范偏向于方式一,更加侧重于这是一个特殊的函数,而非一个特殊的函数名
生成器函数被调用时并不会返回值,而是返回一个“generator object” 的特殊对象,来管理执行流程。
function* generateSequence() {
yield 1;
yield 2;
return 3;
}
// "generator function" 创建了一个 "generator object"
let generator = generateSequence();
// 截止这一步,函数体其实并没有执行
console.info(generator); // [object Generator]
调用next方法才是真正的开始执行
let generator = generateSequence();
let one = generator.next();
// 拿到第一个值后,此时执行在 yield 2; 这一行暂停,等待下一次调用next方法
console.info(JSON.stringify(one)); // {value: 1, done: false}
let two = generator.next();
console.info(JSON.stringify(two)); // {value: 2, done: false}
// 直到第三次,所有值全部返回,显示结束
let three = generator.next();
console.info(JSON.stringify(three)); // {value: 3, done: true}
let four = generator.next();
console.info(JSON.stringify(four)); // {"done":true}
此后此生成器所有的next方法调用都将返回{"done":true}
可迭代
因此,允许使用for of进行循环遍历
使用循环获取值时,当done状态为true时,默认为结束,不显示
因此,不需要使用return返回,仅使用yield即可
function* generateSequence() {
yield 1;
yield 2;
yield 3;
}
let generator = generateSequence();
for(let value of generator) {
console.info(value); // 1,然后是 2,然后是 3
}
// 意味着可以使用spread语法
let sequence = [0, ...generateSequence()];
console.info(sequence); // 0, 1, 2, 3
利用生成器进行迭代
最初的实现,利用自定义可迭代属性,将不可迭代对象进行可迭代处理
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` 范围的所有数字
console.info([...range]); // 1,2,3,4,5
现在使用生成器函数
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() { // [Symbol.iterator]: function*() 的简写形式
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
console.info( [...range] ); // 1,2,3,4,5
组合生成器
yield* 指令将执行 委托 给另一个 generator。
意味着 yield* gen 在 generator gen 上进行迭代,
并将其产出(yield)的值透明地(transparently)转发到外部。
generator 组合(composition)是将一个 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);
}
console.info(str); // 0..9A..Za..z
双向通道
yield:不仅可以向外返回结果,而且还可以将外部的值传递到 generator 内。
- 第一次调用 generator.next() 应该是不带参数的(如果带参数,那么该参数会被忽略)
- yield 的结果进行返回 "2 + 2 = ?"
- yield 接受参数4,赋值给ask1
- yield 的结果进行返回 "3 * 3 = ?"
- yield 接受参数9,赋值给ask2
- 迭代结束,状态为true
function* gen() {
let ask1 = yield "2 + 2 = ?";
console.info(ask1); // 4
let ask2 = yield "3 * 3 = ?"
console.info(ask2); // 9
}
let generator = gen();
console.info( generator.next().value ); // "2 + 2 = ?"
console.info( generator.next(4).value ); // "3 * 3 = ?"
console.info( generator.next(9).done ); // true
// 甚至是异步执行
setTimeout(() => generator04.next(4), 1000);
generator.throw
对于正常值传入使用next方法即可,那对于错误的传入应当使用throw方法
function* gen() {
try {
let result = yield "2 + 2 = ?"; // (1)
console.info("The execution does not reach here, because the exception is thrown above");
} catch(e) {
console.info(e); // 显示这个 error
}
}
let generator = gen(); // *1
let question = generator.next().value; // *2
generator.throw(new Error("The answer is not found in my database")); // (2)
- *1行完成创建生成器对象
- *2行进行生成器的第一次调用,此时返回 "2 + 2 = ?",等待传入值赋值给result
- (2)行传入一个Error,相当于(1)行throw一个Error,直接被catch进行捕获
- 进入catch模块,执行 console.info(e);
- try模块代码被自动忽略
generator.return
完成 generator 的执行并返回给定的 value,此时生成器对象done状态为true,默认进入结束状态
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
异步迭代
回顾可迭代对象
常规可迭代对象通过 Symbol.iterator 方法实现迭代能力:
let range = {
from: 1,
to: 5,
[Symbol.iterator]() {
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) {
console.info(value); // 1, 2, 3, 4, 5
}
异步可迭代对象
当值需要异步获取时(如网络请求),使用 Symbol.asyncIterator:
let range = {
from: 1,
to: 5,
[Symbol.asyncIterator]() {
return {
current: this.from,
last: this.to,
async next() {
await new Promise(resolve => setTimeout(resolve, 1000));
if (this.current <= this.last) {
return { done: false, value: this.current++ };
} else {
return { done: true };
}
}
};
}
};
(async () => {
for await (let value of range) {
console.info(value); // 1, 2, 3, 4, 5(每秒一个)
}
})();
| 特性 | 迭代器 (Symbol.iterator) | 异步迭代器 (Symbol.asyncIterator) |
|---|---|---|
| 方法名 | Symbol.iterator | Symbol.asyncIterator |
next() 返回值 | 任意值 | Promise |
| 循环语法 | for..of | for await..of |
注意:
...展开语法不能与异步迭代器一起工作,因为它期望同步的Symbol.iterator。
异步 Generator
回顾 Generator
常规 generator 使用 function* 和 yield 生成值序列:
function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
yield i;
}
}
for(let value of generateSequence(1, 5)) {
console.info(value); // 1, 2, 3, 4, 5
}
Generator 可作为 Symbol.iterator 使代码更简洁:
let range = {
from: 1,
to: 5,
*[Symbol.iterator]() {
for(let value = this.from; value <= this.to; value++) {
yield value;
}
}
};
异步 Generator
在 function* 前添加 async,即可创建异步 generator:
async function* generateSequence(start, end) {
for (let i = start; i <= end; i++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield i;
}
}
(async () => {
for await (let value of generateSequence(1, 5)) {
console.info(value); // 1, 2, 3, 4, 5(每秒一个)
}
})();
异步 generator 可用作 Symbol.asyncIterator:
let range = {
from: 1,
to: 5,
async *[Symbol.asyncIterator]() {
for(let value = this.from; value <= this.to; value++) {
await new Promise(resolve => setTimeout(resolve, 1000));
yield value;
}
}
};
(async () => {
for await (let value of range) {
console.info(value); // 1, 2, 3, 4, 5
}
})();
技术上可同时实现
Symbol.iterator和Symbol.asyncIterator,但实际中很少这样做。
实际示例:分页数据获取
异步 generator 非常适合处理分页 API,如 GitHub commits:
async function* fetchCommits(repo) {
let url = `https://api.github.com/repos/${repo}/commits`;
while (url) {
const response = await fetch(url, {
headers: {'User-Agent': 'Our script'},
});
const body = await response.json();
// 提取下一页 URL
let nextPage = response.headers.get('Link')?.match(/<(.*?)>; rel="next"/);
url = nextPage?.[1];
for(let commit of body) {
yield commit;
}
}
}
// 使用立即执行函数包裹调用
(async () => {
let count = 0;
for await (const commit of fetchCommits('javascript-tutorial/en.javascript.info')) {
console.log(commit.author.login);
if (++count == 100) break;
}
})();
关键点:
- 内部处理分页逻辑,对外暴露为简单的异步迭代
- 使用
for await..of即可遍历所有 commit,无需关心分页细节