先记住这个答案
普通生成器用 function* 声明,调用 next() 同步返回 { value, done }。异步生成器用 async function* 声明,函数体里既能 await 又能 yield,调用 next() 返回的是一个 Promise,兑现后才是 { value, done }。因为每一步都是异步的,普通的 for...of 用不了,要用 for await...of 来遍历。它最适合「数据要一段一段异步拿到」的场景,比如分页接口、流式读取文件。
- 声明是
async function*,里面能同时写await和yield next()返回 Promise,用for await...of消费- 取一项才往下跑一段,默认串行,不会自动并发
break会触发生成器的finally,适合在里面释放资源
next() 返回值的区别
先把两种生成器各调一次 next(),看拿到的是什么。
yield 1,两种返回JavaScriptfunction* gen() {
yield 1
}
async function* agen() {
yield 1
}
console.log(JSON.stringify(gen().next())) // 直接拿到结果
const p = agen().next()
console.log(p instanceof Promise) // 拿到的是 Promise
p.then((r) => console.log(JSON.stringify(r)))查看输出与解释
{"value":1,"done":false}
true
{"value":1,"done":false}普通生成器的 next() 立刻给出结果。异步生成器的 next() 只给一个 Promise,真正的结果要在 then 里或者 await 之后才拿到,所以最后一行最晚打印。
这就是两者最核心的区别。普通生成器里不能 await,因为 next() 必须同步返回结果,等不了。异步生成器把返回值包成了 Promise,才有了中间等一等的余地。
典型用法:分页接口逐条处理
比如一个列表接口一次返回一页,要一直拉到没有数据为止。用异步生成器把「怎么翻页」封装起来,调用方只管一条一条处理。
const request = (page) =>
new Promise((r) => setTimeout(() => r([`p${page}-a`, `p${page}-b`]), 100)) // 模拟接口
async function* readPages() {
try {
for (let page = 1; page <= 3; page++) {
const items = await request(page) // 等这一页回来
yield* items // 逐条交出去
}
} finally {
console.log('cleanup') // 正常结束或 break 都会走到这里
}
}
async function main() {
for await (const item of readPages()) {
console.log(item)
if (item === 'p2-a') break // 第 3 页根本不会请求
}
console.log('done')
}
main()查看输出与解释
p1-a
p1-b
p2-a
cleanup
done调用 readPages() 时函数体一行都不会执行,等 for await 第一次调用 next() 才开始跑,跑到 yield 就停下,把值交出去。消费者处理完一条,再要下一条,生成器才继续。拉取的节奏由消费者控制,这叫「拉模式」。
循环里 break 时,for await...of 会自动调用生成器的 return(),生成器从暂停的地方直接跳到 finally。所以关闭数据库游标、释放流的 reader 这类清理代码,放在 finally 里最稳。
它不会帮你并发
for await...of 是严格串行的:上一项的 Promise 兑现了,才去要下一项。上面的例子里,三页是一页一页请求的,总耗时是三次请求相加。
所以它和 Promise.all 是两种思路。Promise.all 是一次把请求全发出去,等全部回来;异步生成器是按需一项一项取,好处是能中途停、内存里只放当前这一页。如果想「边处理边预取下一页」,需要自己在生成器里提前发出下一页的请求,这不是语言自带的能力。
容易答错的地方
- 忘了
next()返回的是 Promise - 手动调用异步生成器的
next(),拿到的是 Promise,直接读.value是undefined。要await它,或者直接用for await...of。 - 用普通
for...of遍历异步生成器 - 异步生成器对象只实现了
Symbol.asyncIterator,没有Symbol.iterator,用for...of会抛TypeError,提示它不可迭代。 - 以为
for await会并行执行 - 循环体里的每一轮都要等上一轮完成。想并行得自己用
Promise.all或者手写一个带并发数的预取逻辑。
面试官还会怎么问?
异步生成器里 yield 一个 Promise 会怎样?
异步生成器会先等这个 Promise 兑现,再把兑现后的值交给消费者,所以 for await 拿到的是值本身。如果这个 Promise 拒绝,错误会在生成器内部 yield 的位置抛出,可以用 try...catch 接住。
for await...of 能遍历普通数组吗?
能。遍历同步可迭代对象时,它会对每一项做 await,所以数组里放一组 Promise 也能按顺序拿到结果。但这些 Promise 早就同时开始执行了,这里只是按顺序等,并不是按顺序发起。
异步生成器和 ReadableStream 怎么选?
业务层的分页遍历、轮询、按需加载,用异步生成器写起来最直接。处理字节流、需要 pipeThrough 串联转换、需要浏览器原生的背压和取消传播时,用 ReadableStream。Node.js 和较新的浏览器里,ReadableStream 本身也能用 for await...of 遍历,两者可以配合用,老浏览器要先确认兼容性。
参考资料
示例用于理解所注明的运行环境与边界;延伸学习可结合原文中的更多案例。