generator及其语法糖async/await
其中关于异步操作的处理就引入了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))
生成器对象方法
- 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 不会执行。
它能够中断执行代码的特性,可以帮助我们来控制异步代码的执行顺序
- 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结合是现在的主流也是及有效的方式。
推荐阅读
-
JS异步编程 (2) - Promise、Generator、async/await
-
详解小程序原生使用ES7 async/await语法
-
Promise, Generator, async/await的渐进理解
-
python3.6以上 asyncio模块的异步编程模型 async await语法
-
重新认识 async/await 语法糖
-
C#语法——await与async的正确打开方式
-
【前端】vue前端交互模式-es7的语法结构?async/await
-
Task任务简单了解及其的线程等待和延续、Await/Async关键字简述
-
JS异步编程 (2) - Promise、Generator、async/await
-
前端Tips#6 - 在 async iterator 上使用 for-await-of 语法糖