Generator 函数与协程:yield
和 yield*
的工作机制及异步控制流实现
大家好,今天我们来深入探讨 Generator 函数,以及它们在协程和异步控制流中的应用。Generator 函数是 JavaScript 中一种强大的特性,它允许我们定义可以暂停和恢复执行的函数,这为构建异步代码和处理复杂的数据流提供了极大的灵活性。我们将重点关注 yield
和 yield*
表达式,理解它们的工作机制,并通过实例演示如何利用 Generator 实现异步控制流。
什么是 Generator 函数?
Generator 函数是一种特殊的函数,它使用 function*
关键字声明。与普通函数不同,Generator 函数在调用时不会立即执行,而是返回一个 Generator 对象。这个 Generator 对象是一个迭代器,可以控制 Generator 函数的执行。
核心特性:
- 可暂停和恢复: Generator 函数的执行可以被
yield
表达式暂停,并通过 Generator 对象的next()
方法恢复。 - 惰性求值: Generator 函数只有在调用
next()
方法时才会执行一部分代码,并产生一个值。 - 状态保持: Generator 函数在暂停时会保存当前的状态,包括局部变量和执行上下文,并在恢复时继续使用这些状态。
基本语法:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
const generator = myGenerator();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
在上面的例子中,myGenerator
是一个 Generator 函数。当我们调用 myGenerator()
时,它返回一个 Generator 对象。每次调用 generator.next()
,Generator 函数会执行到下一个 yield
表达式,并将 yield
表达式后面的值作为 value
返回。当 Generator 函数执行完毕时,done
变为 true
。
yield
表达式:暂停与传递
yield
表达式是 Generator 函数的核心。它有两个主要作用:
- 暂停执行: 当 Generator 函数执行到
yield
表达式时,它会暂停执行,并将控制权返回给调用者。 - 传递值:
yield
表达式后面的值会作为next()
方法返回对象的value
属性。
此外,yield
表达式还可以接收 next()
方法传递的值。
示例:
function* myGenerator() {
const value = yield 'Waiting for input...';
console.log('Received:', value);
yield 'Done!';
}
const generator = myGenerator();
console.log(generator.next()); // { value: 'Waiting for input...', done: false }
console.log(generator.next('Hello!')); // Received: Hello! { value: 'Done!', done: false }
console.log(generator.next()); // { value: undefined, done: true }
在这个例子中,第一次调用 generator.next()
时,Generator 函数执行到 yield 'Waiting for input...'
,暂停执行,并将 'Waiting for input...'
作为 value
返回。第二次调用 generator.next('Hello!')
时,'Hello!'
被传递给 yield
表达式,赋值给 value
变量,然后 Generator 函数继续执行,打印 'Received: Hello!'
,并执行到 yield 'Done!'
,再次暂停执行。
总结: yield
允许 Generator 函数在执行过程中暂停并将值传递给调用者,同时还可以接收调用者传递的值,这为实现异步控制流提供了基础。
yield*
表达式:委托 Generator
yield*
表达式用于将 Generator 的执行委托给另一个可迭代对象(通常是另一个 Generator 函数)。它可以简化代码,并将复杂的逻辑分解成更小的、可管理的 Generator 函数。
基本语法:
function* generator1() {
yield 1;
yield 2;
}
function* generator2() {
yield* generator1();
yield 3;
}
const generator = generator2();
console.log(generator.next()); // { value: 1, done: false }
console.log(generator.next()); // { value: 2, done: false }
console.log(generator.next()); // { value: 3, done: false }
console.log(generator.next()); // { value: undefined, done: true }
在这个例子中,generator2
使用 yield* generator1()
将执行委托给 generator1
。这意味着 generator2
会先执行 generator1
的所有 yield
表达式,然后才会继续执行自己的 yield
表达式。
*`yield` 的作用:**
- 代码复用: 可以将通用的 Generator 逻辑提取出来,并在多个 Generator 函数中复用。
- 逻辑分解: 可以将复杂的逻辑分解成更小的、可管理的 Generator 函数,提高代码的可读性和可维护性。
- 简化异步控制流: 可以将异步操作封装到独立的 Generator 函数中,并通过
yield*
将它们组合起来,实现复杂的异步流程。
示例:异步任务的分解
假设我们需要依次执行三个异步任务,每个任务都返回一个 Promise。我们可以将每个任务封装成一个 Generator 函数,然后使用 yield*
将它们组合起来。
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function* task1() {
console.log('Task 1 started');
yield delay(1000);
console.log('Task 1 finished');
return 'Result 1';
}
function* task2() {
console.log('Task 2 started');
yield delay(500);
console.log('Task 2 finished');
return 'Result 2';
}
function* task3() {
console.log('Task 3 started');
yield delay(750);
console.log('Task 3 finished');
return 'Result 3';
}
function* mainTask() {
const result1 = yield* task1();
console.log('Task 1 result:', result1);
const result2 = yield* task2();
console.log('Task 2 result:', result2);
const result3 = yield* task3();
console.log('Task 3 result:', result3);
return 'All tasks completed!';
}
function run(generator) {
return new Promise((resolve, reject) => {
function step(nextFct) {
let next;
try {
next = nextFct();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
v => {
step(() => generator.next(v));
},
err => {
step(() => generator.throw(err));
}
);
}
step(() => generator.next(undefined));
});
}
run(mainTask()).then(result => {
console.log(result); // All tasks completed!
});
在这个例子中,mainTask
使用 yield*
依次执行 task1
、task2
和 task3
。每个任务都返回一个 Promise,run
函数负责处理 Promise 的 resolve 和 reject,并将结果传递给下一个 yield
表达式。
总结: yield*
可以将 Generator 的执行委托给另一个可迭代对象,简化代码,并将复杂的逻辑分解成更小的、可管理的 Generator 函数,尤其在处理异步操作时非常有用。
利用 Generator 实现异步控制流
Generator 函数可以用来实现各种异步控制流模式,例如:
- 顺序执行: 依次执行多个异步任务,每个任务完成后才能执行下一个任务。
- 并行执行: 同时执行多个异步任务,等待所有任务完成后再继续执行。
- 竞态: 同时执行多个异步任务,只要有一个任务完成就继续执行。
- 超时: 在指定时间内完成异步任务,否则取消任务。
下面我们将通过实例演示如何利用 Generator 实现这些异步控制流模式。
1. 顺序执行
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function* task1() {
console.log('Task 1 started');
yield delay(1000);
console.log('Task 1 finished');
return 'Result 1';
}
function* task2() {
console.log('Task 2 started');
yield delay(500);
console.log('Task 2 finished');
return 'Result 2';
}
function* mainTask() {
const result1 = yield task1();
console.log('Task 1 result:', result1);
const result2 = yield task2();
console.log('Task 2 result:', result2);
return 'All tasks completed!';
}
function run(generator) {
return new Promise((resolve, reject) => {
function step(nextFct) {
let next;
try {
next = nextFct();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
v => {
step(() => generator.next(v));
},
err => {
step(() => generator.throw(err));
}
);
}
step(() => generator.next(undefined));
});
}
run(mainTask()).then(result => {
console.log(result); // All tasks completed!
});
在这个例子中,mainTask
使用 yield
依次执行 task1
和 task2
。只有当 task1
完成后,才会执行 task2
。
2. 并行执行
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function* task1() {
console.log('Task 1 started');
yield delay(1000);
console.log('Task 1 finished');
return 'Result 1';
}
function* task2() {
console.log('Task 2 started');
yield delay(500);
console.log('Task 2 finished');
return 'Result 2';
}
function* mainTask() {
const promise1 = run(task1());
const promise2 = run(task2());
const [result1, result2] = yield Promise.all([promise1, promise2]);
console.log('Task 1 result:', result1);
console.log('Task 2 result:', result2);
return 'All tasks completed!';
}
function run(generator) {
return new Promise((resolve, reject) => {
function step(nextFct) {
let next;
try {
next = nextFct();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
v => {
step(() => generator.next(v));
},
err => {
step(() => generator.throw(err));
}
);
}
step(() => generator.next(undefined));
});
}
run(mainTask()).then(result => {
console.log(result); // All tasks completed!
});
在这个例子中,mainTask
同时启动 task1
和 task2
,并使用 Promise.all
等待它们都完成。
3. 竞态
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function* task1() {
console.log('Task 1 started');
yield delay(1000);
console.log('Task 1 finished');
return 'Result 1';
}
function* task2() {
console.log('Task 2 started');
yield delay(500);
console.log('Task 2 finished');
return 'Result 2';
}
function* mainTask() {
const promise1 = run(task1());
const promise2 = run(task2());
const result = yield Promise.race([promise1, promise2]);
console.log('Winner:', result);
return 'Race completed!';
}
function run(generator) {
return new Promise((resolve, reject) => {
function step(nextFct) {
let next;
try {
next = nextFct();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
v => {
step(() => generator.next(v));
},
err => {
step(() => generator.throw(err));
}
);
}
step(() => generator.next(undefined));
});
}
run(mainTask()).then(result => {
console.log(result); // Race completed!
});
在这个例子中,mainTask
同时启动 task1
和 task2
,并使用 Promise.race
等待其中一个完成。
4. 超时
function delay(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function timeout(ms) {
return new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout')), ms));
}
function* task() {
console.log('Task started');
yield delay(2000);
console.log('Task finished');
return 'Result';
}
function* mainTask() {
try {
const result = yield Promise.race([run(task()), timeout(1500)]);
console.log('Result:', result);
} catch (error) {
console.error('Error:', error.message);
}
return 'Task completed!';
}
function run(generator) {
return new Promise((resolve, reject) => {
function step(nextFct) {
let next;
try {
next = nextFct();
} catch (e) {
return reject(e);
}
if (next.done) {
return resolve(next.value);
}
Promise.resolve(next.value).then(
v => {
step(() => generator.next(v));
},
err => {
step(() => generator.throw(err));
}
);
}
step(() => generator.next(undefined));
});
}
run(mainTask()).then(result => {
console.log(result); // Task completed!
});
在这个例子中,mainTask
使用 Promise.race
同时启动 task
和一个超时 Promise。如果 task
在 1500ms 内没有完成,超时 Promise 将 reject,导致 mainTask
抛出错误。
Generator 函数 vs. Async/Await
Async/Await 是 JavaScript 中另一种处理异步操作的方式,它建立在 Promise 之上,并提供了更简洁的语法。虽然 Generator 函数也可以用来实现异步控制流,但 Async/Await 在许多情况下更易于使用和理解。
Async/Await 示例:
async function task1() {
console.log('Task 1 started');
await delay(1000);
console.log('Task 1 finished');
return 'Result 1';
}
async function task2() {
console.log('Task 2 started');
await delay(500);
console.log('Task 2 finished');
return 'Result 2';
}
async function mainTask() {
const result1 = await task1();
console.log('Task 1 result:', result1);
const result2 = await task2();
console.log('Task 2 result:', result2);
return 'All tasks completed!';
}
mainTask().then(result => {
console.log(result); // All tasks completed!
});
对比:
特性 | Generator 函数 | Async/Await |
---|---|---|
语法 | function* , yield , yield* |
async , await |
异步处理 | 需要手动控制 Generator 对象的执行 | 自动处理 Promise 的 resolve 和 reject |
可读性 | 相对复杂,需要理解 Generator 的状态转换 | 更简洁,更接近同步代码的写法 |
适用场景 | 需要更细粒度的控制异步流程,或者需要实现自定义的迭代器 | 大部分异步操作,特别是需要顺序执行的异步任务 |
错误处理 | 需要手动处理 try…catch 块和 generator.throw() |
使用 try…catch 块,与同步代码类似 |
在现代 JavaScript 开发中,Async/Await 通常是处理异步操作的首选方式。然而,理解 Generator 函数的工作机制对于深入理解 JavaScript 的异步编程模型仍然非常重要。
深入理解 Generator 的执行过程
为了更深入地理解 Generator 函数,让我们分析一下 next()
方法的执行过程。
- 首次调用
next()
: 当第一次调用next()
方法时,Generator 函数从函数体的开始处执行,直到遇到第一个yield
表达式。 - 暂停执行: 当遇到
yield
表达式时,Generator 函数暂停执行,并将yield
表达式后面的值作为value
返回给调用者。done
属性设置为false
。 - 传递值: 如果
next()
方法传递了一个值,这个值会被赋值给上一个yield
表达式的左侧变量。 - 恢复执行: 再次调用
next()
方法时,Generator 函数从上一个yield
表达式之后的位置继续执行,直到遇到下一个yield
表达式或函数执行完毕。 - 执行完毕: 当 Generator 函数执行完毕时,
done
属性设置为true
,value
属性设置为return
语句返回的值(如果没有return
语句,则为undefined
)。 throw()
方法: 可以使用throw()
方法向 Generator 函数内部抛出一个错误。这个错误会被try...catch
块捕获,如果没有try...catch
块,Generator 函数会终止执行,并将错误传递给调用者。
理解这些细节对于编写复杂的 Generator 函数和调试异步代码非常有帮助。
Generator在数据流处理中的应用
除了异步控制流,Generator 函数在数据流处理中也有广泛的应用。例如,我们可以使用 Generator 函数来处理大型数据集,避免一次性加载所有数据到内存中。
示例:处理大型日志文件
假设我们有一个大型的日志文件,我们需要逐行读取文件内容并进行处理。我们可以使用 Generator 函数来实现这个功能。
const fs = require('fs');
const readline = require('readline');
function* readLines(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
for await (const line of rl) {
yield line;
}
}
const logFilePath = 'path/to/your/logfile.log'; // 替换为你的日志文件路径
const lineGenerator = readLines(logFilePath);
for (const line of lineGenerator) {
// 在这里处理每一行日志
console.log('Processing line:', line);
}
在这个例子中,readLines
函数使用 readline
模块逐行读取日志文件,并将每一行作为 yield
的值返回。通过使用 for...of
循环,我们可以迭代 readLines
函数返回的 Generator 对象,逐行处理日志文件,而无需将整个文件加载到内存中。
总结
Generator 函数是 JavaScript 中一种强大的特性,它允许我们定义可以暂停和恢复执行的函数,这为构建异步代码和处理复杂的数据流提供了极大的灵活性。yield
表达式用于暂停 Generator 函数的执行并传递值,而 yield*
表达式用于将 Generator 的执行委托给另一个可迭代对象。虽然 Async/Await 在许多情况下更易于使用,但理解 Generator 函数的工作机制对于深入理解 JavaScript 的异步编程模型仍然非常重要。Generator 函数在数据流处理和自定义迭代器等方面也有广泛的应用。
进一步探索的方向
- Redux Saga: Redux Saga 是一个流行的 Redux 中间件,它使用 Generator 函数来处理副作用(例如异步 API 调用)。
- Koa.js: Koa.js 是一个基于 Node.js 的 Web 框架,它大量使用了 Generator 函数来处理请求和响应。
- 自定义迭代器: Generator 函数可以用来创建自定义的迭代器,用于遍历各种数据结构。
希望今天的讲座能够帮助大家更好地理解 Generator 函数,并在实际开发中灵活运用。谢谢大家!