欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

generator及其语法糖async/await

程序员文章站 2022-04-30 09:10:16
...

其中关于异步操作的处理就引入了Promise和生成器。众所周知,Promise可以在一定程度上解决臭名昭著的回调地狱问题。但是在处理多个异步操作时采用Promise链式调用的语法也会显得不是那么优雅和直观。而生成器在Promise的基础上更进一步,允许我们用同步的方式来描述我们的异步流程。

Generator基本原理


generator函数是ES6中一个特殊函数,通过 function* 声明,函数体内通过 yield 来指明函数的暂停点,该函数返回一个迭代器,并且函数执行到 yield语句前面暂停,之后通过调用返回的迭代器next()方法来执行yield语句。

function* generator() {
    yield 1;
    yield 2;
    yield 3
}
var gen = generator()

generator 函数就是ES6中的生成器,生成器又可以生成迭代器,代码执行中断,不会一下执行完,这样我们就可以用同步的方式来描述我们的异步流程。

调用generator函数后,该函数并不执行,返回的也不是函数运行的结果,而是一个指向内部状态的指针对象,我们可以通过调用next方法,使指针移向下一个状态。

console.log(gen.next())  //{ value: 1, done: false }
console.log(gen.next())  //{ value: 2, done: false }
console.log(gen.next())  //{ value: 3, done: false }
console.log(gen.next())  //{ value: undefined, done: true }

Generator 可以实例化出一个 iterator ,并且这个 yield 语句就是用来中断代码的执行的,也就是说,配合 next() 方法,每次只会执行一个 yield 语句。

关于yield插个点

yield 后面可以是任意合法的JavaScript表达式,yield语句可以出现的位置可以等价于一般的赋值表达式(比如a=3)能够出现的位置。

b = 2 + a = 3 // 不合法
b = 2 + (a = 3) // 合法

b = 2 + yield 3 // 不合法
b = 2 + (yield 3) // 合法

复制代码yield关键字的优先级比较低,几乎yield之后的任何表达式都会先进行计算,然后再通过yield向外界产生值。而且yield是右结合运算符,也就是说yield yield 123等价于(yield (yield 123))

生成器对象方法

  1. return方法。和迭代器接口的return方法一样,用于在生成器函数执行过程中遇到异常或者提前中止(比如在for…of循环中未完成时提前break)时自动调用,同时生成器对象变为终止态,无法再继续产生值。也可以手动调用来终止迭代器,如果在调用return方法传入参数,则该参数会作为最终返回对象的value属性值。
function* generator() {
    yield 1
    try {
        yield 2
    } finally {
        yield 3
    }
}
let genl = generator()
console.log(genl.next())    //{ value: 1, done: false }
console.log(genl.next())    //{ value: 2, done: false }
console.log(genl.return(4)) //{ value: 3, done: false }
console.log(genl.next())    //{ value: 4, done: true }
let gen =generator()        
console.log(gen.next())     //{ value: 1, done: false }
console.log(gen.return(4))  //{ value: 4, done: true }
console.log(gen.next())     //{ value: undefined, done: true }
console.log(gen.next())     //{ value: undefined, done: true }

return 会终结整个 Generator ,换句话说:写在 return 后面的 yield 不会执行。
它能够中断执行代码的特性,可以帮助我们来控制异步代码的执行顺序

  1. throw方法。调用此方法会在生成器函数当前暂停执行的位置处抛出一个错误。如果生成器函数中没有对该错误进行捕获,则会导致该生成器对象状态终止,同时错误会从当前throw方法内部向全局传播。在调用next方法执行生成器函数时,如果生成器函数内部抛出错误而没有被捕获,也会从next方法内部向全局传播

分析实现


分析以下码哥:

function* generator() {
    let result = yield 'hello'
    console.log(result)
}
var gen = generator()
// console.log(gen.next())  // { value: 1, done: false }
console.log(gen.next(22)) //{ value: 1, done: false }
console.log(gen.next(2)) //2 { value: undefined, done: true }

第一次调用next方法传入的参数,生成器内部是无法获取到的,或者说没有实际意义,因为此时生成器函数还没有开始执行,第一次调用next方法是用来启动生成器函数的。

const fs = require("fs").promises;
// 生成器
function * read(){
    yield fs.readFile("./name.txt","utf-8")
}
// 迭代器
let it = read()
// console.log(it.next())  // { value: Promise { <pending> }, done: false }
it.next().value.then(data=>{
    console.log(data)  // name
})

这是简单的generator 实例

开始变形
文件:
name.txt: age.txt
age.txt: 666

const fs = require("fs").promises;
function * read(){
    let concent = yield fs.readFile("./name.txt","utf-8")
    let age = yield fs.readFile(concent,"utf-8")
    return age
}
let it = read()
it.next().value.then(data=>{
    it.next(data).value.then(data=>{
        let r = it.next(data)
        console.log(r)  // { value: '666', done: true }
    })
})

才两个就这么麻烦,还好有大神TJ的co库
下载

npm install co 

co 是著名大神 TJ 实现的 Generator 的二次封装库

const fs = require("fs").promises;
function * read(){
    let concent = yield fs.readFile("./name.txt","utf-8")
    let age = yield fs.readFile(concent,"utf-8")
    return age
}
let co = require("co")
co(read()).then(data=>{
    console.log(data)  // 666
})

generator 语法糖 async/await非常好用,代码更简洁了!

const fs = require("fs").promises;
async function read() {
    let concent = await fs.readFile("./name.txt", "utf-8")
    let age = await fs.readFile(concent, "utf-8")
    return age
}
read().then(data => {
    console.log(data) // 666
})

async/await 语法糖


ES7引入的async/await语法是Generator函数的语法糖,只是前者不再需要执行器。直接执行async函数就会自动执行函数内部的逻辑。async函数执行结果会返回一个Promise对象,该Promise对象状态的改变取决于async函数中await语句后面的Promise对象状态以及async函数最终的返回值。接下来重点讲一下async函数中的错误处理。

await关键字之后可以是Promise对象,也可以是原始类型值。如果是Promise对象,则将Promise对象的完成值作为await语句的返回值,一旦其中有Promise对象转化为Rejected状态,async函数返回的Promise对象也会随之转化为Rejected状态

async function aa() {await Promise.reject('error!')}
aa().then(() => console.log('resolved'), e => console.error(e)) // error!

如果await之后的Promise对象转化为Rejected,在async函数内部可以通过try…catch捕获到对应的错误。

async function as() {
  try {
    await Promise.reject('error!')
  } catch(e) {
    console.log(e)
  }
}
as().then(() => console.log('resolved'), e => console.error(e))
// error!
// resolved

如果async函数中没有对转化为Rejected状态的Promise进行捕获,则在外层对调用aa函数进行捕获并不能捕获到错误,而是会把aa函数返回的Promise对象转化为Rejected状态

总结


从一开始的回调函数,到社区大佬们提出后来又加入ES6的Promise,再有generator的生成迭代,到TJ大神的co 库,JavaScript中的代码虽然是单线程的,异步问题的解决方式越来越强,generator的语法糖 async和await 和promise结合是现在的主流也是及有效的方式。