阅读 1192

ES6中的Generator

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

基本介绍

Generator函数和普通函数完全不同,有其与众不同的独特语法。一个简单的Generator函数就长下面这个样子:

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

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

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)能够出现的位置。举例:

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

关于生成器对象

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

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
复制代码

生成器对象还具有以下两个方法:

  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之后也提供了更多管理异步流程的方式,使得我们的代码组织起来更加清晰,更加高效!

关注下面的标签,发现更多相似文章
评论