likes
comments
collection
share

generator及其好用的语法糖async/await

作者站长头像
站长
· 阅读数 5

本文首发CSDN

其中关于异步操作的处理就引入了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结合是现在的主流也是及有效的方式。