ES6中的Generator

ES6中引入不少新特性,其中关于异步操做的处理就引入了Promise和生成器。众所周知,Promise能够在必定程度上解决被广为诟病的回调地狱问题。可是在处理多个异步操做时采用Promise链式调用的语法也会显得不是那么优雅和直观。而生成器在Promise的基础上更进一步,容许咱们用同步的方式来描述咱们的异步流程。app

基本介绍

Generator函数和普通函数彻底不一样,有其不同凡响的独特语法。一个简单的Generator函数就长下面这个样子:异步

function* greet() { yield 'hello' }
复制代码

在第一次调用Generator函数的时候并不会执行Generator函数内部的代码,而是会返回一个生成器对象。在前面的文章中,咱们也提过,经过调用这个生成器对象的next函数能够开始执行Generator函数内部的逻辑,在遇到yield语句会暂停函数的执行,同时向外界返回yield关键字后面的结果。暂停以后在须要恢复Generator函数执行时一样能够经过调用生成器对象的next方法恢复,同时向next方法传入的参数会做为生成器内部当前暂停的yield语句的返回值。如此往复,直到Generator函数内部的代码执行完毕。举例:async

function* greet() {
  let result = yield 'hello'
  console.log(result)
}
let g = greet()
g.next() // {value: 'hello', done: false}
g.next(2) // 打印结果为2,而后返回{value: undefined, done: true}
复制代码

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

yield语法要点

yield 后面能够是任意合法的JavaScript表达式,yield语句能够出现的位置能够等价于通常的赋值表达式(好比a=3)可以出现的位置。举例:ui

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))。spa

关于生成器对象

Generator函数返回的生成器对象是Generator函数的一个实例,也就是说返回的生成器对象会继承Generator函数原型链上的方法。举例:prototype

function* g() {
  yield 1
}
g.prototype.greet = function () {
  console.log('hello')
}
let g1 = g()
console.log(g1 instanceof g) // true
g1.greet() // 'hello'
复制代码

执行生成器对象的[Symbol.iterator]方法会返回生成器对象自己。代理

function* greet() {}
let g = greet()
console.log(g[Symbol.iterator]() === g) // true
复制代码

生成器对象还具备如下两个方法:code

  1. return方法。和迭代器接口的return方法同样,用于在生成器函数执行过程当中遇到异常或者提早停止(好比在for...of循环中未完成时提早break)时自动调用,同时生成器对象变为终止态,没法再继续产生值。也能够手动调用来终止迭代器,若是在调用return方法传入参数,则该参数会做为最终返回对象的value属性值。

若是恰好是在生成器函数中的try代码块中函数执行暂停而且具备finally代码块,此时调用return方法不会当即终止生成器,而是会继续将finally代码块中的逻辑执行完,而后再终止生成器。若是finally代码块中包含yield语句,意味着还能够继续调用生成器对象的next方法来获取值,直到finally代码块执行结束。举例:对象

function* ff(){
  yield 1;
  try{ yield 2 }finally{ yield 3 }
}
let fg = ff()
fg.next() // {value: 1, done: false}
fg.return(4) // {value: 4, done: true}
let ffg = ff()
ffg.next() // {value: 1, done: false}
ffg.next() // {value: 2, done: false}
ffg.return(4) // {value: 3, done: false}
ffg.next() // {value: 4, done: true}
复制代码

从上面的例子中能够看出,在调用return方法以后若是恰好触发finally代码块而且finally代码中存在yield语句,就会致使在调用return方法以后生成器对象并不会当即结束,所以在实际使用中不该该在finally代码块中使用yield语句。

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

yield*语句

yield* 语句是经过给定的Iterable对象的[Symbol.iterator]方法返回的迭代器来产生值的,也称为yield委托,指的是将当前生成器函数产生值的过程委托给了在yield*以后的Iterable对象。基于此,yield* 能够用来在Generator函数调用另一个Generator函数。举例:

function* foo() {
  yield 2
  yield 3
  return 4
}
function* bar() {
  let ret = yield* foo()
  console.log(ret) // 4
}
复制代码

上面的例子中,被代理的Generator函数最终执行完成的返回值最终会做为代理它的外层Generator函数中yield*语句的返回值。

另外,错误也会经过yield*在被委托的生成器函数和控制外部生成器函数的代码之间传递。举例:

function* delegated() {
  try {
    yield 1
  } catch (e) {
    console.log(e)
  }
  yield 2
  throw "err from delegate"
}

function* delegate() {
  try {
    yield* delegated()
  } catch (e) {
    console.log(e)
  }
  yield 3
}

let d = delegate()
d.next() // {value: 1, done: false}
d.throw('err')
// err
// {value: 2, done: false}
d.next()
// err from delegate
// {value: 3, done: false}
复制代码

最后须要注意的是yield*和yield之间的区别,容易忽视的一点是yield*并不会中止生成器函数的执行。举例:

function* foo(x) {
  if (x < 3) {
    x = yield* foo(x + 1)
  }
  return x * 2
}
let f = foo()
f.next() // {value: 24, done: true}
复制代码

使用Generator组织异步流程

使用Generator函数来处理异步操做的基本思想就是在执行异步操做时暂停生成器函数的执行,而后在阶段性异步操做完成的回调中经过生成器对象的next方法让Generator函数从暂停的位置恢复执行,如此往复直到生成器函数执行结束。

也正是基于这种思想,Generator函数内部才得以将一系列异步操做写成相似同步操做的形式,形式上更加简洁明了。而要让Generator函数按顺序自动完成内部定义好的一系列异步操做,还须要经过额外的函数来执行Generator函数。对于每次返回值为非Thunk函数类型的生成器函数,能够用co模块来自动执行。而对于遵循callback的异步API,则须要先转化为Thunk函数而后再集成到生成器函数中。好比咱们有这样的API:

logAfterNs = (seconds, callback) => 
    setTimeout(() => {console.log('time out');callback()}, seconds * 1000)
复制代码

异步流程是这样的:

logAfterNs(1, function(response_1) {
  logAfterNs(2, function () {
    ...
  })
})
复制代码

首先咱们须要将异步API转化为Thunk形式,也就是原来的API:logAfterNs(...args, callback),咱们须要改造为:thunkedLogAfterNs(...args)(callback)

function thunkify (fn) {
  return function (...args) {
    return function (callback) {
      args.push(callback)
      return fn.apply(null, args)
    }
  }
}
let thunkedLogAfterNs = thunkify(logAfterNs)
function* sequence() {
  yield thunkedLogAfterNs(1)
  yield thunkedLogAfterNs(2)
}
复制代码

转化为使用生成器函数来改写咱们的异步流程以后,咱们还须要一个函数来自动管理并执行咱们的生成器函数。

function runTask(gen) {
  let g = gen()
  function next() {
    let result = g.next()
    if (!result.done) result.value(next)
  }
  next()
}

runTask(sequence)
复制代码

更好的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 aa() {
  try {
    await Promise.reject('error!')
  } catch(e) {
    console.log(e)
  }
}
aa().then(() => console.log('resolved'), e => console.error(e))
// error!
// resolved
复制代码

若是async函数中没有对转化为Rejected状态的Promise进行捕获,则在外层对调用aa函数进行捕获并不能捕获到错误,而是会把aa函数返回的Promise对象转化为Rejected状态,在前一个例子中也说明了这一点。

在实验中还尝试使用函数对象做为await关键字以后的值,结果发现await在遇到这种状况时也是按照普通值进行处理,就是await表达式的结果就是该函数对象。

async function bb(){
  let result = await (() => {}); 
  console.log(result);
  return 'done'
}
bb().then(r => console.log(r), e => console.log(e))
// () => {}
// done
复制代码

总结

文章中咱们介绍了Generator函数的基本用法和注意事项,而且也举了一个实际的例子来讲明如何使用Generator函数来描述咱们的异步流程,最后还简单介绍了async函数的使用。总而言之,ES6以后也提供了更多管理异步流程的方式,使得咱们的代码组织起来更加清晰,更加高效!