异步编程进阶:Generator 与 Async/Await
前言
本文将深入了解 ES6 引入的 Generator 函数和 ES2017 (ES7) 引入的 async/await
语法。这两种方式为解决异步流程控制提供了更优雅的方案。
开篇思考
- 调用 Generator 函数 (
function*
) 后,立即返回的是什么? async/await
相比 Promise 和 Generator 的优势体现在哪里?
一、Generator 函数 (ES6/ES2015)
1. 什么是 Generator 函数?
Generator 函数是一种特殊的函数,其定义形式为 function* funcName()
。它允许函数在执行过程中暂停,并在稍后恢复执行。这种暂停和恢复的能力是其用于异步编程的基础。它通过 yield
关键字来控制暂停点。
2. 基本语法与执行
- 定义 : 使用
function*
关键字。 - 暂停与恢复 : 使用
yield
关键字暂停执行并产出一个值。使用 Generator 对象(调用 Generator 函数的返回值)的.next()
方法恢复执行。
代码示例:
javascript
// 1. 定义一个 Generator 函数
function* myGenerator() {
console.log('Generator started...');
const value1 = yield 'Result 1'; // 暂停点 1
console.log('Received from next (1):', value1);
const value2 = yield 'Result 2'; // 暂停点 2
console.log('Received from next (2):', value2);
console.log('Generator finished.');
return 'Final Result'; // 函数最终返回值
}
// 2. 调用 Generator 函数 -> 不会立即执行,返回一个 Generator 对象(迭代器)
const generatorObject = myGenerator();
console.log('Generator object created.');
// 3. 启动/恢复执行:调用 .next()
console.log('Calling next() #1');
let result1 = generatorObject.next(); // 执行到第一个 yield,暂停
console.log(result1); // { value: 'Result 1', done: false }
console.log('\nCalling next() #2 with argument "Hello"');
let result2 = generatorObject.next('Hello'); // 恢复执行,'Hello' 成为上一个 yield 表达式的返回值 (value1)
// 执行到第二个 yield,暂停
console.log(result2); // { value: 'Result 2', done: false }
console.log('\nCalling next() #3 with argument "World"');
let result3 = generatorObject.next('World'); // 恢复执行,'World' 成为上一个 yield 表达式的返回值 (value2)
// 执行到函数末尾 (return)
console.log(result3); // { value: 'Final Result', done: true }
console.log('\nCalling next() #4 after completion');
let result4 = generatorObject.next(); // 函数已完成,再次调用 next 无效
console.log(result4); // { value: undefined, done: true }
关键点总结:
- 惰性执行 :调用 Generator 函数 (
myGenerator()
) 时,函数体内的代码 不会 立即执行,而是返回一个迭代器(Iterator)对象,也称为 Generator 对象。 next()
驱动执行 :每次调用 Generator 对象的.next(value)
方法时,函数会从上次暂停的地方(或函数开头)开始执行,直到遇到下一个yield
语句或return
语句。yield
暂停与产出 :yield
关键字会暂停函数执行,并将yield
后面的表达式的值作为返回的迭代器结果对象的value
属性。.next()
传值 :.next(value)
方法可以向 Generator 函数内部传递一个值,这个值会成为 上一个yield
表达式的返回值。首次调用next()
时传递的参数会被忽略。- 返回结果 :
.next()
方法返回一个包含value
和done
两个属性的对象。value
:yield
表达式产出的值,或者是return
语句返回的值。done
: 布尔值,表示 Generator 函数是否已经执行完毕 (true
表示已完成,false
表示未完成)。
3. yield
关键字详解
yield
是 Generator 函数的核心。它不仅用于暂停和返回值,还构成了 Generator 函数与外部调用者之间的双向通信桥梁。
- 向外传值 :
yield expression
将expression
的值发送给外部调用next()
的代码。 - 从外接收值 :
const receivedValue = yield expression;
,下一次调用next(inputValue)
时,inputValue
会赋值给receivedValue
。
4. yield*
:委托给另一个 Generator 或可迭代对象
yield*
表达式用于将执行权委托给另一个 Generator 函数或任何可迭代对象(如数组、字符串、Map、Set 等)。
代码示例:
javascript
function* generatorA() {
yield 'A1';
yield 'A2';
}
function* generatorB() {
yield 'B1';
yield* generatorA(); // 委托给 generatorA
yield* [ 'B2', 'B3' ]; // 委托给数组
yield 'B4';
}
const genB = generatorB();
console.log(genB.next().value); // B1
console.log(genB.next().value); // A1 (来自 generatorA)
console.log(genB.next().value); // A2 (来自 generatorA)
console.log(genB.next().value); // B2 (来自数组)
console.log(genB.next().value); // B3 (来自数组)
console.log(genB.next().value); // B4
console.log(genB.next().done); // true
二、Generator 与异步编程
Generator 本身是同步的,但其暂停/恢复的特性使其非常适合用来管理异步流程,以同步化的代码风格编写异步逻辑。
1. 面临的问题
直接使用 Generator 处理异步,需要手动在异步操作完成后调用 .next()
来恢复 Generator 的执行。这依然比较繁琐。
2. Thunk 函数(辅助工具)
概念 :Thunk 函数是一种高阶函数,它接收一些参数,并返回一个新的、无参数的函数。这个新函数封装了(或"延迟"了)一个计算或操作。在异步场景下,常用于将 Node.js 风格的回调函数(callback(err, data)
)转换成只接受一个回调函数的 Thunk 函数。
示例(简化版):
javascript
// 一个简单的 Thunk 函数创建器:封装带回调的异步操作
function createThunk(asyncFunction, ...args) {
return function(callback) { // 返回的这个函数就是 Thunk 函数
args.push(callback); // 将真正的回调函数添加到参数列表末尾
asyncFunction.apply(null, args); // 执行原始异步函数
};
}
// 模拟一个异步读取文件的函数 (Node.js fs.readFile 风格)
function fakeReadFile(fileName, callback) {
console.log(`Reading ${fileName}...`);
setTimeout(() => {
if (fileName === 'file1.txt') {
callback(null, 'Content of file 1'); // 成功:error 为 null
} else {
callback(new Error(`File not found: ${fileName}`), null); // 失败:error 对象
}
}, 500);
}
// 使用 createThunk 包装 fakeReadFile
const readFileThunk = (fileName) => createThunk(fakeReadFile, fileName);
// 使用 Thunk
const thunk1 = readFileThunk('file1.txt');
thunk1((err, data) => { // 执行 Thunk 函数,传入最终的回调
if (err) {
console.error('Error reading file:', err);
} else {
console.log('Thunk Result:', data);
}
});
Thunk 在 Generator 异步流程中的作用 :yield
一个 Thunk 函数,然后在 Thunk 的回调中调用 Generator 的 next()
方法,将结果传回。
3. Generator + Thunk 实现异步控制
javascript
function* asyncFlowWithThunks() {
try {
console.log('Starting async flow...');
const result1 = yield readFileThunk('file1.txt'); // yield Thunk
console.log('File 1 content:', result1);
const result2 = yield readFileThunk('file2.txt'); // yield another Thunk
console.log('File 2 content:', result2); // 这行不会执行,因为 file2.txt 会出错
} catch (err) {
console.error('Caught error in Generator:', err.message);
}
console.log('Async flow finished.');
}
// 手动执行器(简化版)
function runThunkGenerator(generatorFunc) {
const generator = generatorFunc(); // 创建 Generator 对象
function nextStep(err, data) { // Thunk 的回调函数,用于驱动 Generator
if (err) {
// 如果 Thunk 返回错误,将错误 throw 进 Generator
return generator.throw(err);
}
// 将数据传回 Generator,并获取下一个 yield 的结果
const result = generator.next(data);
if (result.done) {
// Generator 执行完毕
return;
}
// 如果 yield 的是 Thunk 函数,则执行它,并将 nextStep 作为回调
if (typeof result.value === 'function') {
result.value(nextStep); // 执行 Thunk
} else {
// 如果 yield 的不是 Thunk,可以继续执行或抛错
console.warn('Yielded a non-thunk value:', result.value);
nextStep(null, result.value); // 简单处理,直接把值传回去
}
}
// 启动执行流程
nextStep(null, undefined); // 初始调用
}
// 运行
runThunkGenerator(asyncFlowWithThunks);
这种手动执行器解决了异步流程控制,但如果嵌套层级深,代码依然复杂。
4. Generator + Promise 实现异步控制
Promise 提供了更标准的异步处理方式。Generator 可以 yield
Promise 对象,执行器等待 Promise resolve 后再调用 next()
。
javascript
// 模拟返回 Promise 的异步函数
function readFilePromise(fileName) {
return new Promise((resolve, reject) => {
console.log(`Reading ${fileName} (Promise)...`);
setTimeout(() => {
if (fileName === 'file1.txt') {
resolve(`Promise Content of ${fileName}`);
} else {
reject(new Error(`File not found (Promise): ${fileName}`));
}
}, 500);
});
}
function* asyncFlowWithPromises() {
try {
console.log('Starting async flow (Promises)...');
const result1 = yield readFilePromise('file1.txt'); // yield Promise
console.log('File 1 content:', result1);
const result2 = yield readFilePromise('file2.txt'); // yield another Promise
console.log('File 2 content:', result2); // 这行不会执行
} catch (err) {
console.error('Caught error in Generator (Promise):', err.message);
}
console.log('Async flow (Promises) finished section.');
return "All done successfully!"; // Final return value if no error
}
// 基于 Promise 的 Generator 执行器 (简化版)
function runPromiseGenerator(generatorFunc) {
const generator = generatorFunc();
function handleResult(result) {
if (result.done) {
// Generator 完成,返回最终结果 (包装在 Promise 中)
return Promise.resolve(result.value);
}
// 确保 yield 的值是 Promise
const promise = Promise.resolve(result.value); // 非 Promise 会被包装
return promise.then(
res => handleResult(generator.next(res)), // 成功,将结果传回,继续下一步
err => handleResult(generator.throw(err)) // 失败,将错误 throw 进 Generator
);
}
// 启动流程
try {
return handleResult(generator.next());
} catch (err) {
return Promise.reject(err); // 处理 Generator 内部同步错误
}
}
// 运行
runPromiseGenerator(asyncFlowWithPromises)
.then(finalValue => console.log("Generator Runner Promise Resolved:", finalValue)) // Logs if generator finishes without uncaught error
.catch(err => console.error("Generator Runner Promise Rejected:", err.message)); // Logs if the runner catches an error
这个基于 Promise 的执行器是 async/await
实现的基础。
三、co
函数库 (历史性工具)
目的 : co
是 TJ Holowaychuk 创建的一个著名库,用于 自动执行 Generator 函数。它内部封装了类似上面 runPromiseGenerator
的逻辑,使得开发者无需手动编写执行器。
核心原理:
- 接收一个 Generator 函数作为参数。
- 内部调用 Generator 函数获取迭代器对象。
- 自动调用
next()
,并处理yield
出来的 Promise 或 Thunk。 - 等待 Promise/Thunk 完成后,将结果/错误传回给 Generator 的下一次
next()
或throw()
。 - 重复此过程直到 Generator 执行完毕 (
done: true
)。 co
函数本身返回一个 Promise,该 Promise 在 Generator 成功完成时 resolve,在 Generator 内部出错时 reject。
使用示例:
javascript
// 假设已安装 co: npm install co
const co = require('co'); // 在 Node.js 环境
// 在现代浏览器或打包环境中,可能需要 import co from 'co';
// 使用上面的 asyncFlowWithPromises Generator 函数
co(asyncFlowWithPromises)
.then(finalResult => {
console.log('co execution successful, final value:', finalResult);
})
.catch(err => {
console.error('co caught an error:', err.message);
});
现状 : co
库在 async/await
普及后,其使用场景已大大减少。async/await
提供了原生、更简洁的语法来完成同样的目标。
四、async/await
(ES2017/ES7) - 终极解决方案
async/await
是基于 Promise 和 Generator 构建的语法糖,旨在以更接近同步代码的方式书写异步逻辑。
1. 基本语法
async
关键字:用于声明一个函数是异步函数。异步函数会自动将其返回值包装成一个 Promise 对象。await
关键字:只能在async
函数内部使用。它会暂停async
函数的执行,等待await
右侧的 Promise 对象变为 resolved 状态,然后将 Promise 的解决值作为await
表达式的结果继续执行。如果 Promise 被 rejected,await
会抛出这个 rejection 的错误(可以被try...catch
捕获)。
2. async/await
重写示例
javascript
// 使用上面返回 Promise 的 readFilePromise 函数
async function mainAsyncAwait() {
console.log('Starting async/await flow...');
try {
const result1 = await readFilePromise('file1.txt'); // 等待 Promise resolve
console.log('File 1 content (await):', result1);
const result2 = await readFilePromise('file2.txt'); // 等待 Promise resolve
console.log('File 2 content (await):', result2); // 这行不会执行
console.log('Async/await flow finished successfully.'); // 也不会执行
return "All tasks completed via async/await!"; // 函数的返回值会被包装成 Promise
} catch (err) {
console.error('Caught error in async function:', err.message);
// 即使捕获了错误,async 函数仍然会返回一个 rejected 的 Promise
// 如果希望函数正常结束(返回 resolved Promise),可以在 catch 中 return 一个值
return "Finished with errors handled.";
}
}
// 调用 async 函数
mainAsyncAwait()
.then(finalMsg => {
console.log('Async function promise resolved with:', finalMsg);
})
.catch(err => {
// 通常 async 函数内部的 catch 会处理错误,这里不会触发,除非 async 函数本身抛出未捕获异常
console.error('Async function promise rejected:', err);
});
3. async/await
对比 Generator + co
的优势
- 内置执行器 :
async
函数自带执行器,无需像 Generator 那样依赖co
或手动编写执行器。调用async
函数就像调用普通函数一样简单。 - 适用性更广 :
await
关键字后面理论上可以跟任何值。如果不是 Promise,该值会被自动包装成一个 resolved 的 Promise (Promise.resolve(value)
)。而co
通常要求yield
的是 Promise 或 Thunk。 - 语义更清晰 :
async
和await
的语义比function*
和yield
更直接地表达了异步操作的意图,代码可读性更好。 - 原生语法支持: 作为 ECMAScript 标准的一部分,无需引入任何第三方库。
- 错误处理 : 使用标准的
try...catch
语句捕获await
操作中的同步和异步错误,非常自然。
4. async
函数的返回值
async
函数总是隐式地返回一个 Promise。- 如果
async
函数内部使用return value;
,则返回的 Promise 会以value
resolve。 - 如果
async
函数内部抛出未捕获的错误,则返回的 Promise 会以该错误 reject。 - 如果没有
return
语句,则返回的 Promise 会以undefined
resolve。
代码示例:
javascript
async function simpleAsync() {
console.log("Inside async function");
return 42; // 返回值会被包装
}
async function errorAsync() {
throw new Error("Something went wrong");
}
const promise1 = simpleAsync();
console.log(promise1); // Promise { <pending> } (之后会 resolve 为 42)
promise1.then(value => console.log('simpleAsync resolved:', value)); // 42
const promise2 = errorAsync();
console.log(promise2); // Promise { <pending> } (之后会 reject)
promise2.catch(err => console.error('errorAsync rejected:', err.message)); // Something went wrong
五、异步编程方式对比总结
特性 | 回调函数 (Callback Hell) | Promise (ES6) | Generator + Runner/co |
async/await (ES2017) |
---|---|---|---|---|
核心 | 函数嵌套调用 | Promise 对象链 (.then ) |
Generator + 执行器 | async 函数 + await |
写法风格 | 嵌套回调 | 链式调用 | 近似同步 (需执行器) | 近似同步 (原生) |
可读性 | 差 | 较好 | 好 | 最好 |
错误处理 | 分散在各回调参数 | .catch() / try...catch |
try...catch (需执行器支持) |
try...catch (原生) |
控制流 | 复杂 | 线性/并行 (Promise.all) | 线性 | 线性 |
依赖 | 无 | 原生 (ES6+) | 原生 Generator + 库/手动 | 原生 (ES2017+) |
现代推荐 | 避免 | 基础,常与async/await 结合 |
不常用 | 推荐 |
六、回答开篇思考
-
调用 Generator 函数 (
function*
) 后,立即返回的是什么? 答:调用 Generator 函数并不会立即执行函数体内的代码,而是立即返回一个 Generator 对象(迭代器 Iterator) 。你需要通过调用这个对象的.next()
方法来启动或恢复函数的执行。 -
async/await
相比 Promise 和 Generator 的优势体现在哪里? 答:async/await
的主要优势在于:- 语法简洁,可读性极高:代码看起来非常像同步代码,易于理解和维护。
- 内置执行器 :不需要像 Generator 那样依赖
co
库或手动编写执行器来自动处理异步流程。 - 错误处理自然 :可以使用标准的
try...catch
语句捕获await
等待的 Promise 的 rejection,以及函数内的同步错误。 - 原生标准:是 ECMAScript 语言规范的一部分,无需额外库。
- 调试友好:在调试器中单步跟踪异步代码流更加直观。