进阶篇:Generator 和 Async 函数

Generator 函数(生成器)是 ES2015 提供的可暂停函数,常被用来组织异步流程。Async 函数是 ES2017 提供的写法,把 Generator 加自动执行器包成更顺手的语法。现行异步代码以 async / await 为主,Generator 仍用于迭代器和惰性序列。

Async 函数可以看作 Generator 函数的语法糖,本质是 Generator 函数加上执行器。

Generator 函数

Generator 是一种特殊函数,最大特点是可以控制函数的执行(执行时能暂停,之后能从暂停处继续)。

语法上,Generator 函数是一个普通函数,但是有以下特征:

  • 函数声明中,function 关键字与函数名之间有一个星号(*);
  • 函数体内部使用 yield(英语里的意思就是“产出”) 关键字来暂停和恢复 Generator 函数;
  • Generator 函数调用后,并不立即执行,而是返回一个指向内部状态的指针对象,该对象是一个迭代器对象。
  • 调用返回的迭代器对象的 next() 方法,会移动内部指针,使得指针指向下一个状态。next() 方法会返回一个对象,该对象包含 value 和 done 属性,value 的值是 yield 后面表达式的值;done 属性表示 Generator 函数是否执行完毕,即是否还有下一个阶段。
function* genFunc(x) {
    const y = 2 * (yield (x + 1))
    const z = yield (y / 3)
    return (x + y + z)
}
const it = genFunc(5)
console.log(it.next())   // {value: 6, done: false}
console.log(it.next(12)) // {value: 8, done: false}
console.log(it.next(13)) // {value: 42, done: true}

代码执行分析:

  • 调用 Generator 函数(可以传参),返回一个迭代器;
  • 当执行第一次 next 时,传参会被忽略,并且函数暂停在 yield (x + 1) 处,所以返回 5 + 1 = 6
  • 当执行第二次 next 时,传入的参数 12 就会被当作上一个 yield 表达式的返回值,此时 let y = 2 * 12,所以第二个 yield 等于 2 * 12 / 3 = 8注意: 如果 next 不传参,yield 永远返回 undefined
  • 当执行第三次 next 时,传入的参数 13 就会被当作上一个 yield 表达式的返回值,所以 z = 13, x = 5, y = 24,相加等于 42。

注意: Generator 函数不能当构造器使用。

yield 关键字

yield 关键字用来暂停和恢复一个 Generator 函数。

yield [expression]

yield 关键字使 Generator 函数执行暂停,其后面的表达式的值返回给 Generator 函数的调用者。返回值是一个迭代器执行结果的对象,有两个属性,value 和 done。value 属性是对 yield 表达式求值的结果,而 done 是false,表示 Generator 函数尚未完全完成;如果 Generator 函数已完成,则 valueundefined(或 return 指定的值),donetrue

Generator 函数的执行,一旦遇到 yield 表达式,将被暂停,直到 Generator 函数的 next() 方法再次调用。每次调用 next() 方法,Generator 函数都会恢复执行,直到达到以下某个值:

  • yield:Generator 函数再次暂停,返回给调用者的值,value 为 yield 表达式的值并且 donefalse。下一次调用 next() 时,从 yield 之后继续。
  • throw:从 Generator 函数中抛出异常。函数停止执行,和普通抛错一样。
  • 到达 Generator 函数的结尾:执行结束,返回给调用者,valueundefined 并且 donetrue
  • 到达 return 语句:执行结束,value 由 return 指定,并且 donetrue
function* generatorFunc(x) {
    yield 1
    // throw '2'   // 抛出异常:Uncaught 2
    return 0
    console.log(3) // 永远不会执行
}
const it = generatorFunc()
console.log(it.next()) // {value: 1, done: false}
console.log(it.next()) // {value: 0, done: true}
console.log(it.next()) // {value: undefined, done: true}

注意: yield 表达式如果用在另一个表达式之中,必须放在圆括号里面。

function* generatorFunc(x) {
    const result1 = 2 + (yield x + 5)
    }
const it = generatorFunc(1)
console.log(it.next()) // {value: 6, done: false}

注意: yield 只能在 Generator 函数中使用,在其他地方使用会报错。

注意: yield 后面的操作是同步的。也就是说,如果是 yield 后面是异步操作,调用 next() 方法时,执行 Generator 函数会立即往后执行,不会等待当前停留的 yield 表达式异步操作完成。

next() 方法

返回一个包含 value 和 done 属性的对象。该方法也可以通过接受一个参数用以向 Generator 函数传值。

gen.next(value)

next() 方法中的参数将成为 Generator 函数上一个 yield 表达式的返回值。

function* generatorFunc(x) {
    const y = 2 * (yield (x + 1))
    const z = yield (y / 3)
    return z
}
const it = generatorFunc(5)
console.log(it.next(10)) // 传参将忽略
console.log(it.next())   // {value: NaN, done: false},相当于 const y = 2 * undefined
console.log(it.next(13)) // {value: 13, done: true},相当于 const z = 13

注意: 由于 next() 方法的参数是表示上一个 yield 表达式的返回值,所以在第一次使用 next() 方法时,传递参数是无效的。

next() 方法返回的对象包含两个属性:

  • value:迭代器返回的任意值,即 yield 表达的返回结果。当 done 的值为 true 时,可以返回指定值,也可以忽略该值。
  • done:布尔值。Generator 函数尚未完成,则值为 false;如果已完成,则返回 true。

throw() 方法

用来向 Generator 函数抛出异常。

如果 throw() 抛出的异常能被 Generator 函数内的 try...catch 块捕获,则不会中断 Generator 函数执行,仍会执行到下一个 yield。也就是说,throw() 方法相当于执行了一次 next() 方法,只是跳到 catch 块中,并执行了其中的代码。

function* genFunc() {
    try {
        yield 1
    } catch (e) {
        console.log(e)
    }
    yield 2
    yield 3
}
const it = genFunc()
console.log(it.next())
console.log(it.throw('飘过,你们继续'))
console.log(it.next())

// {value: 1, done: false}
// 飘过,你们继续
// {value: 2, done: false}
// {value: 3, done: false}

如果 Generator 函数未定义 try...catch 块,但 throw() 抛出的异常能被外部 try...catch 块捕获,则 Generator 函数执行结束,程序继续执行。

function* genFunc() {
    yield 1
    yield 2
    yield 3
}
try {
    const it = genFunc()
    console.log(it.next())
    console.log(it.throw('你完了,但其他人还会好好的'))
    console.log(it.next())
} catch (e) {
    console.log(e)
}
console.log('是的,我们还很好')

// {value: 1, done: false}
// 你完了,但其他人还会好好的
// 是的,我们还很好

如果 throw() 抛出的异常,即未在 Generator 函数内被捕获,也没有被外部 try...catch 块捕获,则返回 throw() 抛出的异常,程序中断执行。

function* genFunc() {
    yield 1
    yield 2
    yield 3
}
const it = genFunc()
console.log(it.next())
console.log(it.throw('毁灭吧,我累了'))
console.log(it.next())

// {value: 1, done: false}
// Uncaught 毁灭吧,我累了

注意: throw 方法抛出的错误要被内部捕获,前提是必须至少执行过一次 next() 方法。

return() 方法

用于结束 Generator 函数的执行,并返回给定的值。

function* genFunc(x) {
    yield 1
    yield 2
}
const it = genFunc()
console.log(it.next())
console.log(it.return('我睡了,你们继续'))
console.log(it.next())
console.log('接着奏乐接着舞')

// {value: 1, done: false}
// {value: '我睡了,你们继续', done: true}
// {value: undefined, done: true}
// 接着奏乐接着舞

yield* 表达式

yield 表达式用于操作一个可迭代对象,并产生它返回的每个值。yield 表达式本身的值是当迭代器关闭时返回的值(即 done 为 true 时)。

function* genFunc1(x) {
    yield 1
    yield 2
    yield 3
}
function* genFunc2(x) {
    yield 4
    yield genFunc1()
    yield 5
}
for (let v of genFunc2()) {
    console.log(v);
}
// 4
// genFunc1 {<suspended>}
// 5

yield* 表达式用于遍历一个可迭代对象。

将上述 yield genFunc1() 改成 yield * genFunc1(),得到结果:

function* genFunc2(x) {
    yield 4
    yield* genFunc1()
    yield 5
}
// 4
// 1
// 2
// 3
// 5

异步编程的执行器

使用 Generator 函数可以解决异步编程的问题:

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err); return }
            console.log(data)
            resolve(data)
        })
    })
}
function * genRead(value) {
    const r1 = yield read(value)
    const r2 = yield read(r1)
    const r3 = yield read(r2)
}

const it = genRead('./1.txt');
it.next().value.then((r1) => {
    it.next(r1).value.then((r2) => {
        it.next(r2).value.then(() => {
            console.log('全部完成!')
        })
    })
})

该解决方案存在两个问题:

  • 需要手动调用每个步骤的执行。
  • 存在代码层级嵌套问题,不利于阅读和调试。

以下是一个 Generator 函数异步编程的自动执行器,实现的原理是,保证每个 yield 返回的都是函数,并且当前 yield 返回的函数将下一个 yield 返回的函数作为回调函数,即,回调函数嵌套。

const fs = require('fs')
// 将 yield 表达式转换为函数,作为迭代器返回的对象中 value 的值
function thunk(file) {
    return function (cb) {
        return fs.readFile(file, 'utf8', (err, data) => {
            if (err) { return }
            console.log(data)
            cb(data)
        })
    }
}
// 实现 Generator 函数异步编程的自动执行
function autoRunner(gen, value) {
    const it = gen(value);
    function next(data) {
        const result = it.next(data);
        if (result.done) {
            return;
        }
        result.value(next);
    }
    next();
}

const genFunc = function* (value){
    const f1 = yield thunk(value);
    const f2 = yield thunk(f1);
    const f3 = yield thunk(f2);
};
autoRunner(genFunc, './1.txt');

另外,也可以使用第三方包实现自动执行,比如历史上的 coco 已停止维护,新代码应直接用 async / await

co 是 TJ Holowaychuk 于 2013 年发布的小工具,曾用来自动执行 Generator。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err); return }
            console.log(data)
            resolve(data)
        })
    })
}
function * geneRead(value) {
  const r1 = yield read(value)
  const r2 = yield read(r1)
  const r3 = yield read(r2)
}

const co = require('co')
co(geneRead('./1.txt')).then(() => {
    console.log('全部完成!')
})

其他应用场景

Iterator 生成函数

const myIterable = {};
myIterable[Symbol.iterator] = function* () {
    yield 1;
    yield 2;
};
const it = myIterable[Symbol.iterator]();
console.log(it.next()); // {value: 1, done: false}
console.log(it.next()); // {value: 2, done: false}
console.log(it.next()); // {value: undefined, done: true}

多维数组转一维数组

function* iterArr(arr) {
    if (Array.isArray(arr)) {
        for (let i = 0; i < arr.length; i++) {
            yield* iterArr(arr[i]);
        }
    } else {
        yield arr;
    }
}
const arr = [
    'a',
    ['b', 'c'],
    ['d', ['e', 'f']]
];
for (let v of iterArr(arr)) {
    console.log(v); // a b c d e f
}
// 或者
console.log([...iterArr(arr)]); // ['a', 'b', 'c', 'd', 'e', 'f']

Async 函数

Async 函数是 ES2017 提供的异步写法。最大优点是代码清晰,让异步逻辑看起来像同步。现行项目里这是最常用的方案,并不是语言层面「再也没有后继者」的终极形态。

本质上,Async 函数是 Generator 函数的语法糖,是 Generator 函数 + Promise 的组合。

其实现原理,就是将 Generator 函数和自动执行器,包装在一个函数里,类似上述讲到的 Generator + co。

const fs = require('fs')
function read(file) {
    return new Promise((resolve, reject) => {
        fs.readFile(file, 'utf8', (err, data) => {
            if (err) { reject(err); return }
            console.log(data)
            resolve(data)
        })
    })
}

async function promiseRead() {
    const r1 = await read('./1.txt')
    const r2 = await read(r1)
    const r3 = await read(r2)
}
promiseRead();

async 函数

async 函数是使用 async 关键字声明的函数,函数体内允许使用 await 关键字。

async function promiseRead() {
    await read('./1.txt')
}

Async 函数一定会返回一个 Promise 对象,即使不是,也会被隐式地包装在一个 Promise 中。该 Promise 要么会通过一个由 async 函数返回的值被解决,要么会通过一个从 async 函数中抛出的(或其中没有被捕获到的)异常被拒绝。

await 关键字

await 用于等待一个 Promise。它用在 async 函数里;ES2022 起也可以用在模块顶层。

await 会暂停当前 async 函数,等 Promise 敲定后再继续。兑现时,兑现值就是 await 表达式的值;拒绝时,把拒绝原因作为异常抛出,若未捕获则中断该 async 函数(返回的 Promise 变为拒绝)。

如果 await 后面不是 Promise,则得到该值本身(相当于 await Promise.resolve(x))。

注意: 普通脚本(非模块)的顶层、普通函数里写 await 会抛语法错。

注意: 为避免 await 后面的 Promise 拒绝中断整个 async 函数,通常把 await 放在 try...catch 里,或在 Promise 上接 catch

相关问题

next()、throw()、return() 的有何异同?

next()、throw()、return() 都能让 Generator 函数恢复执行,并且执行到下一个 yield 表达式。

next() 将 yield 表达式的结果替换成一个指定的值,不会结束 Generator 函数的执行。

throw() 向暂停点注入异常。若内部 try...catch 接住了,生成器可以继续跑到下一个 yield;否则生成器结束,异常甩给调用方。它不是把 yield「换成一个值」。

return() 将 yield 表达式的结果替换成一个指定的值,会结束 Generator 函数的执行。

Generator 与 Async 对比?

  • Generator 函数出现在 ES2015 中,Async 函数出现在 ES2017 中,Async 是 Generator 的语法糖。
  • Generator 函数需要手动逐步执行或者使用自动执行器(如,co);而 Async 函数自带执行器,执行方式与普通函数相同。
  • Generator 函数的 * 号和 yield 的语义没那么清晰;Async 函数的 async 表示异步,await 表示等待,语义更加清楚。
  • Generator 函数的 yield 后面可以是任意表达式;用 Generator 做异步时,执行器(如历史上的 co)通常要求 yield 后面是 Thunk 或 Promise。await 后面可以是 Promise,也可以是普通值(会当成已兑现的 Promise)。
  • Generator 函数返回遍历器;Async 函数返回 Promise 对象。

顶层 await

早期的语法规定是,await 命令只能出现在 async 函数内部,否则都会报错。

function genPromise() {
    return Promise.resolve(1)
}
const r1 = await genPromise()
console.log(r1)

报错:

Uncaught SyntaxError: await is only valid in async functions and the top level bodies of modules

从 ES2022 开始,允许在模块的顶层独立使用 await 命令,主要目的是为解决模块异步加载的问题。

参考资料

MDN - function*

MDN - async 函数

阮一峰 - Generator 函数的语法

阮一峰 - async 函数

© lizhao all right reserved,powered by Gitbook文件修订时间: 2026-08-31 22:19:32

results matching ""

    No results matching ""