生成器最常见于 sagas
中,但它们有更多的使用场景。你将在本文中看到其中的一些。
对于“什么是生成器?”这个问题的简短回答是:
生成器是 JavaScript 中的拉取流(pull stream)。
现在我们来剖析这个定义,然后跳入一些示例中。
首先,你需要理解两个术语:“拉取(pull)”和“流(stream)”。
什么是流?
流是随时间传递的数据。流有两种类型:推送流(push stream)和拉取流(pull stream)。
什么是推送流?
推送流是一种机制,你无法控制数据何时到达。
推送流的例子包括:
- websocket,
- 从磁盘读取文件,
- 服务器发送事件。
你可以在下面看到一个使用 Node.js 从磁盘读取大文件的推送流 JavaScript 示例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 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)
1 2 3 4 5 6 7 8 9 10 11
| const numbers = [1, 2, 3, 4, 5];
const squares = numbers.map(num => { console.log(`Squaring ${num}`); return num * num; });
console.log('squares:', squares); console.log('squares:', squares);
|
你可能会想:“好吧,但为什么 Promise 是饥渴的呢?它们的结果是延迟到达的。”
JavaScript 中的 Promise 表现出饥渴求值有以下几个原因。
- 立即执行:传递给新 Promise 的函数(称为执行器函数)在 Promise 构造时立即执行。
- 不可逆操作:一旦执行器函数开始执行,无法通过消费代码停止或暂停。它执行的操作(无论是成功解析还是拒绝)会被排队到 JavaScript 事件循环中尽快处理。
- 没有惰性选项:Promise 没有内置机制来推迟或取消执行器的执行,直到需要值时才开始。
- 副作用:Promise 的饥渴特性意味着,任何包含在执行器中的副作用(如 API 调用、超时或 I/O 操作)都会立即发生。
下面的示例演示了 Promise 的立即执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
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); });
|
输出结果如下:
1 2 3 4 5
| $ node eager-promise-example.js Before promise Inside promise executor After promise Resolved data
|
惰性
惰性意味着只有在需要值时才进行求值(而不是提前)。拉取流就是惰性的。
一个同步的例子是操作数选择运算符。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
|
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);
|
运行这段代码时,你会看到如下输出:
1 2 3
| $ node lazy-evaluation-example.js 惰性求值开始 结果: false
|
由于 isDataProcessed
是 false
,因此 processData
函数从未执行,你也不会在控制台看到 “Processing 5”。这表明表达式只会计算得到结果所需的部分。
什么是生成器?
生成器是 JavaScript 中的拉取流。这意味着它是一种特殊的函数,你可以暂停执行并稍后恢复。
生成器函数返回的 Generator 对象符合可迭代协议和迭代器协议。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| function* myGenerator() { yield "Hire senior"; yield "React engineers"; yield "at ReactSquad.io"; }
const iterator = myGenerator();
console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next()); console.log(iterator.next());
for (let string of myGenerator()) { console.log(string); }
|
除了 .next()
方法,生成器还有 .return()
和 .throw()
方法。
.return()
- .return()
方法终止生成器的执行并返回指定的值,还会触发任何 finally
代码块。
.throw()
- .throw()
方法允许在生成器的最后一个 yield
处抛出一个错误,该错误可以被捕获和处理,或者允许生成器通过 finally
代码块进行清理。如果未捕获,它会停止生成器并将其标记为完成。
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 32 33
| function* numberGenerator() { try { yield 1; yield 2; yield 3; } finally { console.log("清理完成"); } }
const generator = numberGenerator();
console.log(generator.next()); console.log(generator.next());
console.log(generator.return(10));
console.log(generator.next());
const newGenerator = numberGenerator(); console.log(newGenerator.next());
try { newGenerator.throw(new Error("出错了")); } catch (e) { console.log(e.message); }
console.log(newGenerator.next());
|
你也可以在调用 .next()
时传入数字或其他值。
试着预测以下示例中的日志输出。
1 2 3 4 5 6 7 8 9 10 11 12 13
| 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()
时收到的输入操作和生成值。
看看输出,并检查你的预测。
1 2 3 4 5 6 7 8 9 10
| const it2 = moreNumbers(40);
console.log(it2.next());
console.log(it2.next(2012));
console.log(it2.next());
|
我们逐步分析 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 (没有新的输入),完成执行。 |
生成器的使用场景
生成器有三大主要使用场景:
- 惰性求值 - 按需生成数据或处理大型或无限的数据集。
- 异步编程 - 处理异步操作。
- 迭代器 - 允许在复杂流程的各个步骤之间暂停执行。
之前你看到了将从磁盘读取文件作为推送流的示例。下面是如何使用生成器将其转换为拉取流的代码:
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 32 33 34 35 36 37
| 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
。
通常,当你想控制何时获取一个值时,会使用生成器。
来看下面这个测试示例:
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 32 33 34 35 36 37
| 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
数组中的位置是未知的,而生成器是一个拉取流,因此你可以按需获取角色值。