生成器最常见于 sagas
中,但它们有更多的使用场景。你将在本文中看到其中的一些。
对于“什么是生成器?”这个问题的简短回答是:
生成器是 JavaScript 中的拉取流(pull stream)。
现在我们来剖析这个定义,然后跳入一些示例中。
首先,你需要理解两个术语:“拉取(pull)”和“流(stream)”。
什么是流?
流是随时间传递的数据。流有两种类型:推送流(push stream)和拉取流(pull stream)。
什么是推送流?
推送流是一种机制,你无法控制数据何时到达。
推送流的例子包括:
- websocket,
- 从磁盘读取文件,
- 服务器发送事件。
你可以在下面看到一个使用 Node.js 从磁盘读取大文件的推送流 JavaScript 示例:
const fs = require('fs');
const readStream = fs.createReadStream('./largeFile.txt');
readStream.on('data', chunk => {
console.log('data received', chunk.length);
});
readStream.on('end', () => {
console.log('finished reading file');
});
readStream.on('error', error => {
console.log('an error occurred while reading the file', error);
});
什么是拉取流?
拉取流是指你可以控制何时请求数据。
你很快会在生成器代码中看到 JavaScript 的拉取流示例,但首先你需要理解另一个概念。
惰性 vs. 饥渴
在编程中,数据处理可以通过两种基本方式进行:饥渴(eager)或惰性(lazy)。
饥渴
饥渴意味着数据会立即被评估,无论当下是否需要结果。推送流就是饥渴的。(其他例子:数组方法、Promise)
// 使用数组方法的饥渴求值:
const numbers = [1, 2, 3, 4, 5];
// Map 方法会立即处理数组中的所有元素。
const squares = numbers.map(num => {
console.log(`Squaring ${num}`);
return num * num;
});
console.log('squares:', squares); // [1, 4, 9, 16, 25]
console.log('squares:', squares); // [1, 4, 9, 16, 25]
你可能会想:“好吧,但为什么 Promise 是饥渴的呢?它们的结果是延迟到达的。”
JavaScript 中的 Promise 表现出饥渴求值有以下几个原因。
- 立即执行:传递给新 Promise 的函数(称为执行器函数)在 Promise 构造时立即执行。
- 不可逆操作:一旦执行器函数开始执行,无法通过消费代码停止或暂停。它执行的操作(无论是成功解析还是拒绝)会被排队到 JavaScript 事件循环中尽快处理。
- 没有惰性选项:Promise 没有内置机制来推迟或取消执行器的执行,直到需要值时才开始。
- 副作用:Promise 的饥渴特性意味着,任何包含在执行器中的副作用(如 API 调用、超时或 I/O 操作)都会立即发生。
下面的示例演示了 Promise 的立即执行。
// 使用 Promise 和数组方法的饥渴求值
console.log("Before promise");
let promise = new Promise((resolve, reject) => {
console.log("Inside promise executor");
resolve("Resolved data");
});
console.log("After promise");
promise.then(result => {
console.log(result);
});
输出结果如下:
$ node eager-promise-example.js
Before promise
Inside promise executor
After promise
Resolved data
惰性
惰性意味着只有在需要值时才进行求值(而不是提前)。拉取流就是惰性的。
一个同步的例子是操作数选择运算符。
// 使用逻辑运算符进行惰性求值
function processData(data) {
console.log(`Processing ${data}`); // 这行代码永远不会被执行 🚫
return data * data;
}
console.log('惰性求值开始');
const data = 5;
const isDataProcessed = false;
// 使用逻辑与运算符进行惰性求值。
const result = isDataProcessed && processData(data);
console.log('结果:', result); // false
运行这段代码时,你会看到如下输出:
$ node lazy-evaluation-example.js
惰性求值开始
结果: false
由于 isDataProcessed
是 false
,因此 processData
函数从未执行,你也不会在控制台看到 “Processing 5”。这表明表达式只会计算得到结果所需的部分。
什么是生成器?
生成器是 JavaScript 中的拉取流。这意味着它是一种特殊的函数,你可以暂停执行并稍后恢复。
生成器函数返回的 Generator 对象符合可迭代协议和迭代器协议。
function* myGenerator() {
yield "Hire senior";
yield "React engineers";
yield "at ReactSquad.io";
}
const iterator = myGenerator();
// 将生成器用作迭代器
console.log(iterator.next()); // { done: false, value: "Hire senior" }
console.log(iterator.next()); // { done: false, value: "React engineers" }
console.log(iterator.next()); // { done: false, value: "at ReactSquad.io" }
console.log(iterator.next()); // { done: true, value: undefined }
// 将生成器用作可迭代对象
for (let string of myGenerator()) {
console.log(string); // "Hire senior", "React engineers", "at ReactSquad.io"
}
除了 .next()
方法,生成器还有 .return()
和 .throw()
方法。
-
.return()
-.return()
方法终止生成器的执行并返回指定的值,还会触发任何finally
代码块。 -
.throw()
-.throw()
方法允许在生成器的最后一个yield
处抛出一个错误,该错误可以被捕获和处理,或者允许生成器通过finally
代码块进行清理。如果未捕获,它会停止生成器并将其标记为完成。
function* numberGenerator() {
try {
yield 1;
yield 2;
yield 3;
} finally {
console.log("清理完成");
}
}
const generator = numberGenerator();
// 正常使用生成器
console.log(generator.next()); // { done: false, value: 1 }
console.log(generator.next()); // { done: false, value: 2 }
// 使用 return() 提前结束生成器
console.log(generator.return(10)); // { done: true, value: 10 }
// 在 return() 之后,不再生成新的值
console.log(generator.next()); // { done: true, value: undefined }
// 为 throw 示例重置生成器
const newGenerator = numberGenerator();
console.log(newGenerator.next()); // { done: false, value: 1 }
// 使用 throw() 抛出错误信号
try {
newGenerator.throw(new Error("出错了"));
} catch (e) {
console.log(e.message); // "出错了"
}
// 在 throw() 之后,生成器关闭
console.log(newGenerator.next()); // { done: true, value: undefined }
你也可以在调用 .next()
时传入数字或其他值。
试着预测以下示例中的日志输出。
function* moreNumbers(x) {
console.log('x', x);
const y = yield x + 2;
console.log('y', y);
const z = yield x + y;
console.log('z', z);
}
const it2 = moreNumbers(40);
console.log(it2.next());
console.log(it2.next(2012));
console.log(it2.next());
此示例演示了生成器函数 moreNumbers
如何根据每次调用 .next()
时收到的输入操作和生成值。
看看输出,并检查你的预测。
const it2 = moreNumbers(40);
// x: 40
console.log(it2.next()); // { value: 42, done: false }
// y: 2012
console.log(it2.next(2012)); // { value: 2052, done: false }
// z: undefined
console.log(it2.next()); // { value: undefined, done: true }
我们逐步分析 moreNumbers
生成器函数的每一步,以便你充分理解它。
步骤 | 代码行 | 控制台输出 | 解释 |
---|---|---|---|
1 | const it2 = moreNumbers(40) |
初始化生成器,x 设置为 40。 |
|
2 | console.log(it2.next()); |
{ value: 42, done: false } |
生成器启动并将 x 记录为 40,然后生成 42 (x + 2 )。 |
3 | console.log(it2.next(2012)); |
{ value: 2052, done: false } |
继续执行,y 为 2012,记录 y ,生成 2052 (x + y )。 |
4 | console.log(it2.next()); |
{ value: undefined, done: true } |
继续执行,z 为 undefined (没有新的输入),完成执行。 |
生成器的使用场景
生成器有三大主要使用场景:
- 惰性求值 - 按需生成数据或处理大型或无限的数据集。
- 异步编程 - 处理异步操作。
- 迭代器 - 允许在复杂流程的各个步骤之间暂停执行。
之前你看到了将从磁盘读取文件作为推送流的示例。下面是如何使用生成器将其转换为拉取流的代码:
const fs = require('fs');
function getChunkFromStream(stream) {
return new Promise((resolve, reject) => {
stream.once('data', (chunk) => {
stream.pause();
resolve(chunk);
});
stream.once('end', () => {
resolve(null);
});
stream.once('error', (err) => {
reject(err);
});
stream.resume();
});
}
async function* readFileChunkByChunk(filePath) {
const stream = fs.createReadStream(filePath);
let chunk;
while (chunk = await getChunkFromStream(stream)) {
yield chunk;
}
}
const generator = readFileChunkByChunk('./largeFile.txt');
(async () => {
for await (const chunk of generator) {
console.log("数据接收", chunk.length);
}
})();
实际案例
Sagas
是处理异步 I/O 操作的一个典型例子。但你将在 Redux 系列文章中学习如何使用 Sagas
。
通常,当你想控制何时获取一个值时,会使用生成器。
来看下面这个测试示例:
test('当用户为已加入的组织所有者时:显示邀请链接创建 UI 以及组织成员,并允许用户更改其角色', async ({ page }) => {
// 组织中角色的生成器。
function* roleGenerator() {
const allRoles = Object.values(ORGANIZATION_MEMBERSHIP_ROLES);
for (const role of allRoles) {
yield role;
}
}
const roleIterator = roleGenerator();
const data = await setup({
page,
role: ORGANIZATION_MEMBERSHIP_ROLES.OWNER,
numberOfOtherTeamMembers: allRoles.length,
});
const { organization, sortedUsers, user } = data;
// 导航到团队成员设置页面。
await page.goto(`/organizations/${organization.slug}/settings/team-members`);
// 通过生成器为每个团队成员分配角色。
for (let index = 0; index < sortedUsers.length; index++) {
const memberListItem = page.getByRole('list', { name: /team members/i }).getByRole('listitem').nth(index);
const otherUser = sortedUsers[index];
// 为每个团队成员更改角色,除了当前用户。
if (otherUser.id !== user.id) {
await memberListItem.getByRole('button', { name: /member/i }).click();
const role = roleIterator.next().value!;
await page.getByRole('option', { name: role }).getByRole('button').click();
await page.keyboard.press('Escape');
}
}
await teardown(data);
});
在此测试中,定义了一个 roleGenerator
来顺序提供组织用户的角色列表。该方法允许测试在 UI 中的角色管理功能中为每个用户动态分配预定义列表中的唯一角色。
在这个例子中使用生成器而不是数组的原因是,测试中主用户在 sortedUsers
数组中的位置是未知的,而生成器是一个拉取流,因此你可以按需获取角色值。
网友评论