JavaScript Generator函数与async/await用法详解

作者:闭包旅人时间:2026-10-31 04:56:26浏览:176

本文详细介绍JavaScript中Generator函数的定义、执行机制及next/throw/return方法的使用,并结合async/await语法探讨异步编程的演进。通过对比Generator与async函数的特性,解析自动执行器的实现原理及性能优化建议,帮助开发者深入理解JS异步处理的核心概念。

Generator函数

Generator 函数在语法结构上看似与普通函数不同,但其本质仍属于常规函数范畴,主要具备两个显著特征。

1、function关键字与函数名之间必须包含一个星号(*)。

2、函数体内部需使用yield表达式来定义不同的内部状态(yield在英语中意为“产出”),该关键字具有暂停执行和等待后续调用的语义。

调用Generator函数后,其执行行为与普通函数类似,但当代码执行至 yield 关键字时会暂停(暂停),必须等待调用 next 方法后才会继续(继续)执行后续代码,直至再次遇到 yield 或 return 关键字。

当 yield 作为 表达式 表达式的一部分使用时,必须使用 () 将其括起来,否则会导致语法错误,例如代码:2 * (yield x)。

调用 next 方法会指示 generator 函数从当前位置继续往后执行语句,默认执行逻辑会到达下一个 下一个 yield 语句,并将 yield 后的内容 包装在返回对象的 value 属性中。若未遇到 return 或 函数结束,则返回对象中的 done 属性值为 false;若遇到终止条件,done 属性值则为 true。

return 方法用于标识函数的结束,其返回的内容同样可以通过 next 方法获取;此外,若函数中未显式编写 return,其行为与 return undefined 类似。

可参考以下代码案例进行理解。

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
    return 5  // { value: 5, done: true },再往后 next 就和不写一样了
    // 不写return,则与return undefined 一样 返回 { value: undefined, done: true }
}
//获取 generator 遍历器
let g = generator()
let next = null
do {
    next = g.next() //开始往后执行到下一个 yield 语句,并返回
    console.log(next)
}while(next?.done === false) //执行到 return 或 函数结束 则停止打印
next = g.next()
console.log(next)
// { value: 1, done: false }
// { value: 2, done: false }
// { value: 3, done: false }
// { value: 4, done: false }
// 有return 5 打印 { value: 5, done: true }, 没有则打印 { value: undefined, done: true }

若在 class 类中使用,表示方式略有不同。由于类方法默认带有构造函数包装,只需在方法名前添加一个 *  即可实现相同功能。

class A {
    //前面加上 * 即可
    * generator() {
        yield 1
        yield 2
    }
}
let a = new A()
let g = a.generator()
let next = g.next()
console.log(next)
next = g.next()
console.log(next)

generator 与 Iterator遍历器

从上述打印结果可以看出,generator函数 实际上返回的是一个 Iterator遍历器 对象,因此我们也可以利用遍历器的机制来执行 generator遍历器。

下面演示如何使用 for ... of 语法来遍历上述生成的 generator遍历器。

//从上面就可以看出,generator函数 返回了一个 Iterator遍历器对象
//我们遍历一次试试
let g = generator()
for (let item of g) {
    console.log(item)
}
//结果打印 1 2 3 4

观察运行结果发现,最后定义的 return 并未被打印输出。了解遍历器机制可知,当遍历器遇到 done 属性值为 true 时会直接终止遍历(因此,若将 return 语句 用于存放遍历内容,需注意此限制)。

我们再对比验证一下,确认我们的 generator遍历器 是否确实属于 Iteraotr遍历器,结果显示两者 一模一样。

//g[Symbol.iterator]() === g //遍历器属性一致,返回为 true,不信打印遍历器属性比较试试

next

前文已介绍,调用 generator遍历器 的 next() 方法后,执行器会继续运行至下一个 yield语句。

此外,next 方法支持 传参 参数,我们传入的 传递的参数 会作为 上一个 yield 返回的结果 的值返回;若未传递参数,则默认返回 undefined。

注意:yield 返回的结果 不是 该表达式后面的内容,且当 yield 作为表达式一项时,需要使用 () 括起来,否则会报错。

通过以下案例即可直观看到结果(执行过程已在代码中标注)。

//包含表达式时,yield 语句需要用括号括起来,否则会报错
function* generator() {
    let num1 = yield 1
    console.log('num1', num1)
    let num2 = num1 * (yield 2)
    console.log('num2', num2)
    yield 3
    yield 4
}
//next传入的值,作为上一个 yield 的返回值使用
let g = generator()
let next = g.next() //此时执行到 yield 1,并包装返回 yield 1 执行的结果
console.log(next)  //{ value: 1, done: false }
//传入 2, 当做上一个语句的 yield 1 的返回值(不传接收到的值均为undefined)
//即:返回 2 赋值给 num,然后执行到 (yield 2),并包装返回 (yield 2) 的结果
next = g.next(2) //执行完毕后,打印:num1 2
console.log(next) //{ value: 2, done: false }
//传入 3. 当做上一个语句的 (yield 2) 的返回值(不传接收到的值均为undefined)
//即:num2 = num1 * 3, 然后执行到 yield 3,并包装返回 yield 3 的结果
next = g.next(3)  //执行完毕后,打印:num2 6
console.log(next) //{ value: 3, done: false }

由上述分析可见,next 传参 功能十分实用,除上述用法外,甚至可以利用传递的参数来跳出死循环等场景。

throw

此处的 throw 是指通过 generator 遍历器抛出的异常,该异常会抛出到 generator 函数里面 内部,因此需要注意控制抛出 异常的位置 的位置,以便更好地配合 try...catch 结构使用。

//throw()
// 相当于在 generator 里面抛出了一个异常
function* generator() {
    try {
        yield 1
    }catch(err) {
        console.log(err)
    }
    yield 2
    yield 3
    yield 4
}
let g = generator()
let next = g.next()
console.log(next)
//执行完毕 yield 1后,随后在 yield 1 ~ yield 2 之间抛出异常,因此,try...catch 包裹主 yield 1 才行
let t = g.throw('啦啦啦') // 抛出异常并执行到下一个 yield,如果没处理错误,程序异常
console.log(t)
next = g.next()
console.log(next)

return

指定调用 return 会立即结束 generator 函数 的执行,并返回 { value: undefined, done: true },前文已有介绍。

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
}
let g = generator()
next = g.next()
next = g.return() //直接返回 { value: undefined, done: true },如果传参则返回return传递的参数
console.log(next) //会直接结束

另外,当存在 try...finally 代码块且执行流进入 try 部分时,return 操作会触发 仍然执行finally finally 块内的语句,并执行至 finally里面 中的 yield 语句;finally 执行完毕后,才是真正的函数结束。此时,return 带入的参数会应用到 finally 语句的 末尾 末尾。

function* generator() {
    yield 1
    try {
        //执行到这里,外面 return 的话,也需要走完 finally 才会结束
        yield 2
        yield 3
    }finally {
        yield 7
        yield 8
    }
    yield 4
}
let g1 = generator()
next = g1.next() // { value: 1, done: false }
next = g1.next() // { value: 2, done: false }
next = g1.return(100) // { value: 7, done: false },return 后返回下一个,为finally中的 yield 7,这里顺道返回一个值测试一下最后的返回值
next = g1.next() // { value: 8, done: false }
next = g1.next() // { value: 100, done: true }
next = g1.next() // { value: undefined, done: true }

yield*

yield* 看似一个指针,其指向一个 generator遍历器,会自动展开嵌套的生成器,此处不再赘述。

//yield* 会展开 generator 函数
function* gen1() {
    yield 1
    yield 2
}
function* gen2() {
    yield 0
    yield* gen1()
}
//相当于下面展开的语句,可以看出yield语句总是一个一个执行的,即使嵌套也不会一次执行一堆
function* gen3() {
    yield 0
    yield 1
    yield 2
}

generator应用

generator 的应用场景广泛,此处仅简要介绍两种基础用法,以此略作尝试。

正常切换开关通常需要维护状态,此处通过 generator 遍历器避免了新增开关状态的需求(注:此方法不适用于多方控制场景,例如需要与后台远程同步状态的情况)。

//切换状态
function* toggleSwitch() {
    while(true) {
        console.log('开灯')
        yield 1
        console.log('关灯')
        yield 0
    }
}
//是不是切换很简单了
let t = toggleSwitch()
let next = t.next() //开灯
console.log(next)
next = t.next() //关灯
console.log(next)

流程化管理中,外部调用者无需关心内部逻辑(例如:加工软件中,不同工种只需关注自己的操作工序,完成后点击一次 next 即可推进流程)。

function* order() {
    yield '收到订单'
    yield '加工'
    yield '送检'
    yield '质检'
    yield '发货'
    yield '完成订单'
}
let o = order()
let next = o.next() //收到订单
next = o.next() //加工
next = o.next() //送检
next = o.next() //质检
next = o.next() //发货
next = o.next() //完成订单

async、await函数

async、await 是我们日常开发中较为常用的语法,此处作简要介绍。

async、await 是基于 generator函数 改造而成的,它改进了 generator函数 作为普通函数时的痛点。其内部加入了 执行器,使得函数能像普通函数一样直接执行,更加 方便,且 语义更加清晰 语义更加清晰,最终结果返回一个 Promise(可以看出其内部使用了Promise进行包装)。

简而言之:async 用于声明一个异步函数,await 用于等待执行,当 await 语句执行完毕后,才会执行后续代码(若出现错误则直接抛出异常并结束执行,不再向后继续)。此外,如果没有 await 的存在,async 函数的执行顺序与普通同步函数并无区别。

ps:关于 promise 前文已有介绍,await 的执行过程与其类似,实际上会立即将任务加入队列,待前面的任务执行完毕后,才会执行 await 后面的语句。

简单做一个使用案例,

async function a() {
    await promise1...
    await promise2...
    // 如果不return自己内容,返回的则是 undefind
    return 1
}
function cc() {
    a().then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err)
    })
}

你可能会想,如果第一个 promise 出错了怎么办,第二个会执行么?

答案:不会,第一个出错后,会抛出一个异常,此时会结束解释器的执行,直接反馈错误到外部。

下面改进上一个案例

function a1() {
    return Promise.reject('出错了 ')
}
function a2() {
    return Promise.resolve('成功了 ')
}
async function a() {
    await a1()
    console.log(11111111111)
    await a2()
    // 如果不return自己内容,返回的则是 undefind
    return 1
}
function cc() {
    a().then(res => {
        console.log(res)
    }).catch(err => {
        console.log(err)
    })
}
//结果是没有执行打印 11111111111,且执行到了 .catch 中

下面我们模拟传递多个图片的案例,可以看出 async、await 应用多么方便。

function uploadAImage(fileUrl) {
    return new Promise(function(resolve, reject) {
        setTimeout(() => {
            let res = fileUrl + '成功了'
            console.log(res)
            resolve(res)
        }, 500);
    })
}
async function uploadImages() {
    //准备多个图片url
    let fileUrl = ['url1', 'url2', 'url3']
    for (url of fileUrl) {
        try {
            await uploadAImage(url)
        }catch(err) {
            console.log(err)
            //实际上更复杂,成功的下次就不需要在上传了
            return Promise.reject('存在上传失败的内容') 
        }
    }
    return '都成功了'
}
uploadImages().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

async、await 函数使用简单,就先介绍到这里了,后续会对 generator 进行对比,解析其是如何被改造的。

generator 模仿 async 自动执行

前文提到 async 函数 是通过 generator 函数 改造而来的,内部添加了执行器,我们自己尝试编写一个简易的 generator 执行器。

这里是一个async 的默认执行案例

//使用 generator + 执行器,简单翻译一下 async、await
async function asyncDefaultFunction() {
    let res = await new Promise((resolve) => {
        setTimeout(() => {
            resolve(1)
        }, 1000);
        // setTimeout(() => {
        //     reject('啦啦啦')
        // }, 1000);
    })
    return res
}
asyncDefaultFunction().then(res => {
    console.log(res)
})

我们使用 generator 改造成 async 自动执行的模式

//使用 async 之后,实际上我们将返回函数包装一下即可,并且将 yield 就是 await
function asyncFunction() {
    return co(function* () {
        //async 函数里面的内容,将 yield 与 await 替换即可
        let res = yield new Promise((resolve, reject) => {
            setTimeout(() => {
                resolve(1)
            }, 1000);
            // setTimeout(() => {
            //     reject('啦啦啦')
            // }, 1000);
        })
        return res
    })
}
//编写一个自动执行 generator 的 co 函数
function co(genFunc) {
    return new Promise(function (resolve, reject) {
        const g = genFunc() //获取generator,准备执行
        function nextFunc(value) {
            let next;
            try {
                next = g.next(value)
            }catch(err) {
                return reject(err)
            }
            if (next.done) {
                return resolve(next.value)
            }
            //为什么要promise包装,语句可能为同步函数,可能为异步函数,木事保证正确执行
            //如果为 promise 直接原封不动返回,如果为普通对象则包装,具体见promise
            Promise.resolve(next.value).then(function(res) {
                nextFunc(res)
            }, function(err) {
                reject(err)
            })
        }
        nextFunc(undefined)
    })
}
asyncFunction().then(res => {
    console.log(res)
}).catch(err => {
    console.log(err)
})

这样就实现了一个简易的 generator 自动执行器

同时我们也发现了,async 与我们普通函数相比,加剧了 负担,为了优化性能,我们平时如果没有用到异步函数(或者连 await 都用不到的),将多余的 async 去掉吧,这样某种程度上能够避免性能浪费,尤其是到了循环语句。

最后

看到了这里,相信我们也能学习到不少东西,可以想一下,其他语言是不是也是类似这样呢,遇到了相似的情况我们是不是也豁然开朗了呢,很多语言是互通的,学习就是积累的过程,希望我们此次能有所收获!

以上就是JS中Generator函数与async函数用法介绍的详细内容,更多关于JS Generator函数与async函数的资料请关注本站其它相关文章!

作者最新文章