# YDKJS-生成器

JavaScript 开发者在代码中几乎普遍依赖的一个假定:一个函数一旦开始执行,就会运行到结束,期间不会有其他代码能够打断它并插入其间。不过 ES6 引入了一个新的函数类型,它并不符合这种运行到结束的特性。这类新的函数被称为生成器。

var x = 1
function bar() {
  x++
}
function* foo() {
  x++
  yield // 暂停
  console.log('x: ', x)
}

生成器就是一类特殊的函数,可以一次或多次启动和停止,并不一定非得要完成。

# 输入和输出

生成器函数虽然特殊,但它仍然是一个函数,这意味着它仍然有一些基本的特性没有改变。比如,它仍然可以接受参数(即输入),也能够返回值(即输出)。

调用生成器会得到一个迭代器对象,通过上面的 next(..) 方法可以让生成器开始执行,然后停在下一个 yield 处或者直到生成器结束。

其中 next(..) 调用的结果是一个对象,它有一个 value 属性,持有从生成器中返回的值(如果有的话)。换句话说,yield 会导致生成器在执行过程中发送出一个值,这有点类似于中间的 return

除了能够接受参数并提供返回值之外,生成器甚至提供了更强大更引人注目的内建消息输入输出能力,通过 yieldnext(..) 实现:

function* foo(x) {
  var y = x * (yield 1)
  return y
}
var it = foo(6)
it.next().value // 1
var res = it.next(7)
res.value // 42

消息是双向传递的——yield.. 作为一个表达式可以发出消息响应 next(..) 调用,next(..) 也可以向暂停的 yield 表达式发送值。

# 多个迭代器

从语法使用的方面来看,通过一个迭代器控制生成器的时候,似乎是在控制声明的生成器函数本身。但有一个细微之处很容易忽略:每次构建一个迭代器,实际上就隐式构建了生成器的一个实例,通过这个迭代器来控制的是这个生成器实例。

function* foo() {
  var x = yield 2
  z++
  var y = yield x * z
  console.log(x, y, z)
}
var z = 1
var it1 = foo()
var it2 = foo()
var val1 = it1.next().value // 2 <-- yield 2
var val2 = it2.next().value // 2 <-- yield 2
val1 = it1.next(val2 * 10).value // 40 <-- x:20, z:2
val2 = it2.next(val1 * 5).value // 600 <-- x:200, z:3
it1.next(val2 / 2)
// 20 300 3
it2.next(val1 / 4)
// 200 10 3

通过生成器让函数交替执行(甚至在语句当中)已成为可能。

# 生成器产生值

假定你要产生一系列值,其中每个值都与前面一个有特定的关系。要实现这一点,需要一个有状态的生产者能够记住其生成的最后一个值。

var gimmeSomething = (function() {
  var nextVal
  return function() {
    if (nextVal === undefined) {
      nextVal = 1
    } else {
      nextVal = 3 * nextVal + 6
    }
    return nextVal
  }
})()

gimmeSomething() // 1
gimmeSomething() // 9
gimmeSomething() // 33
gimmeSomething() // 105

示例直接使用函数闭包来实现,实际上,这个任务是一个非常通用的设计模式,通常通过迭代器来解决。

迭代器是一个定义良好的接口,用于从一个生产者一步步得到一系列值。JavaScript 迭代器的接口,与多数语言类似,就是每次想要从生产者得到下一个值的时候调用 next()

可以为我们的数字序列生成器实现标准的迭代器接口:

var something = (function(max) {
  var nextVal
  return {
    // for..of循环需要
    [Symbol.iterator]: function() {
      return this
    },
    // 标准迭代器接口方法
    next: function() {
      if (nextVal === undefined) {
        nextVal = 1
      } else {
        nextVal = 3 * nextVal + 6
      }
      if (nextVal >= max) {
        return { done: true, value: nextVal }
      }
      return { done: false, value: nextVal }
    },
  }
})(1000)

something.next().value // 1
something.next().value // 9
something.next().value // 33
something.next().value // 105

ES6 还新增了一个 for..of 循环,这意味着可以通过原生循环语法自动迭代标准迭代器。

# iterable

前面例子中的 something 对象叫作迭代器,因为它的接口中有一个 next() 方法。而与其紧密相关的一个术语是 iterable(可迭代),即指一个包含可以在其值上迭代的迭代器的对象。

从 ES6 开始,从一个 iterable 中提取迭代器的方法是:iterable 必须支持一个函数,其名称是专门的 ES6 符号值 Symbol.iterator。调用这个函数时,它会返回一个迭代器。通常每次调用会返回一个全新的迭代器,虽然这一点并不是必须的。

生成器本身并不是 iterable,尽管非常类似——当你执行一个生成器,就得到了一个迭代器:

function* something() {
  var nextVal
  while (true) {
    if (nextVal === undefined) {
      nextVal = 1
    } else {
      nextVal = 3 * nextVal + 6
    }
    yield nextVal
  }
}

这里的 something 就是生成器,并不是 iterable。我们需要调用 something() 来构造一个生产者供 for..of 循环迭代:

for (var v of something()) {
  console.log(v)
  // 不要死循环
  if (v > 500) {
    break
  }
}
// 1 9 33 105 321 969

看起来似乎 *something() 生成器的迭代器实例在循环中的 break 调用之后就永远留在了挂起状态。

其实有一个隐藏的特性会帮助你管理此事。for..of 循环的“异常结束”(也就是“提前终止”),通常由 breakreturn 或者未捕获异常引起,会向生成器的迭代器发送一个信号使其终止。

尽管 for..of 循环会自动发送这个信号,但你可能会希望向一个迭代器手工发送这个信号。可以通过调用 return(..) 实现这一点。

var it = something()
for (var v of it) {
  console.log(v)
  if (v > 500) {
    console.log(it.return('Hello World').value)
    // 这里不需要 break
  }
}

调用 it.return(..) 之后,它会立即终止生成器。如果生成器内部有 try...finally 代码块,且正在执行 try 代码块,那么 return 方法会导致立刻进入 finally 代码块,执行完以后,整个函数才会结束。

事实上,如果生成器内有 try..finally 语句,它将总是运行,即使生成器已经外部结束。

# 异步迭代生成器

同样的功能我们使用迭代器来实现:

function foo(x, y, cb) {
  ajax('http://some.url.1/?x=' + x + '&y=' + y, cb)
}
foo(11, 31, function(err, text) {
  if (err) {
    console.error(err)
  } else {
    console.log(text)
  }
})

function foo(x, y) {
  ajax('http://some.url.1/?x=' + x + '&y=' + y, function(err, data) {
    if (err) {
      // 向 *main() 抛出一个错误
      it.throw(err)
    } else {
      // 用收到的 data 恢复 *main()
      it.next(data)
    }
  })
}
function* main() {
  try {
    var text = yield foo(11, 31)
    console.log(text)
  } catch (err) {
    console.error(err)
  }
}
var it = main()
// 这里启动
it.next()

从本质上而言,我们把异步作为实现细节抽象了出去,使得我们可以以同步顺序的形式追踪流程控制:“发出一个 Ajax 请求,等它完成之后打印出响应结果。”

当然,我们只在这个流程控制中表达了两个步骤,而这种表达能力是可以无限扩展的,以便我们无论需要多少步骤都可以表达。

更精彩的部分在于 yield 暂停使我们不仅能够从异步函数调用得到看似同步的返回值,还可以同步捕获来自这些异步函数调用的错误。

# 生成器 +Promise

对于 yield 的出来的 promise,迭代器会侦听这个 promise 的决议(完成或拒绝),然后要么使用完成消息恢复生成器运行,要么向生成器抛出一个带有拒绝原因的错误。

function foo(x, y) {
  return request('http://some.url.1/?x=' + x + '&y=' + y)
}
function* main() {
  try {
    var text = yield foo(11, 31)
    console.log(text)
  } catch (err) {
    console.error(err)
  }
}

var it = main()
var p = it.next().value
// 等待 promise p 决议
p.then(
  function(text) {
    it.next(text)
  },
  function(err) {
    it.throw(err)
  },
)

现在,我们利用了已知 *main() 中只有一个需要支持 Promise 的步骤这一事实。如果想要能够实现 Promise 驱动的生成器,不管其内部有多少个步骤呢?

# 支持 Promise 的 Generator Runner

我们当然不希望每个生成器手工编写不同的 Promise 链,所以需要有一种方法可以实现重复(即循环)迭代控制:

function run(gen) {
  var args = [].slice.call(arguments, 1),
    it
  // 在当前上下文中初始化生成器
  it = gen.apply(this, args)

  // 返回一个 promise 用于生成器完成
  return Promise.resolve().then(function handleNext(value) {
    // 对下一个 yield 出的值运行
    var next = it.next(value)
    return (function handleResult(next) {
      // 生成器运行完毕了吗?
      if (next.done) {
        return next.value
      }
      // 否则继续运行
      else {
        return Promise.resolve(next.value).then(
          // 成功就恢复异步循环,把决议的值发回生成器
          handleNext,
          // 如果 value 是被拒绝的 promise,
          // 就把错误传回生成器进行出错处理
          function handleErr(err) {
            return Promise.resolve(it.throw(err)).then(handleResult)
          },
        )
      }
    })(next)
  })
}

现在,这种运行 run(..) 的方式,它会自动异步运行你传给它的生成器,直到结束。

# 生成器中的 Promise 并发

到目前为止,我们已经展示的都是 Promise+ 生成器下的单步异步流程。但是,现实世界中的代码常常会有多个异步步骤:你需要从两个不同的来源获取数据,然后把响应组合在一起以形成第三个请求,最终把最后一条响应打印出来。

function* foo() {
  var r1 = yield request('http://some.url.1')
  var r2 = yield request('http://some.url.2')
  var r3 = yield request('http://some.url.3/?v=' + r1 + ',' + r2)
  console.log(r3)
}

run(foo)

这段代码可以完成我们的需求,但是不是最优的。现在的逻辑中第二个请求必须等待第一个请求完成才会发起,而更好的效果是前面两个请求可以同时触发。

但是 yield 只是代码中一个单独的暂停点,并不可能同时在两个点上暂停。如何才能让两个请求并发运行呢?最自然有效的答案就是让异步流程基于 Promise:

function* foo() {
  // 让两个请求"并行"
  var p1 = request('http://some.url.1')
  var p2 = request('http://some.url.2')
  // 等待两个 promise 都决议
  var r1 = yield p1
  var r2 = yield p2
  var r3 = yield request('http://some.url.3/?v=' + r1 + ',' + r2)
  console.log(r3)
}

run(foo)

作为一个风格方面的提醒:要注意你的生成器内部包含了多少 Promise 逻辑。我们介绍的使用生成器实现异步的方法的全部要点在于创建简单、顺序、看似同步的代码,将异步的细节尽可能隐藏起来。

比如,这可能是一个更简洁的方案:

function bar(url1, url2) {
  return Promise.all([request(url1), request(url2)])
}
function* foo() {
  // 隐藏 bar(..) 内部基于 Promise 的并发细节
  var results = yield bar('http://some.url.1', 'http://some.url.2')
  var r1 = results[0]
  var r2 = results[1]
  var r3 = yield request('http://some.url.3/?v=' + r1 + ',' + r2)
  console.log(r3)
}

run(foo)

# 生成器委托

你可能会从一个生成器调用另一个生成器,使用辅助函数 run(..),就像这样:

function* foo() {
  var r2 = yield request('http://some.url.2')
  var r3 = yield request('http://some.url.3/?v=' + r2)
  return r3
}
function* bar() {
  var r1 = yield request('http://some.url.1')
  // 通过 run(..) "委托"给 *foo()
  var r3 = yield run(foo)
  console.log(r3)
}

run(bar)

还有一个更好的方法可以实现从 *bar() 调用 *foo(),称为 yield 委托:

function* foo() {
  var r2 = yield request('http://some.url.2')
  var r3 = yield request('http://some.url.3/?v=' + r2)
  return r3
}
function* bar() {
  var r1 = yield request('http://some.url.1')
  // 通过 yeild* "委托"给 *foo()
  var r3 = yield* foo()
  console.log(r3)
}
run(bar)

yield* 暂停了迭代控制,而不是生成器控制。当你调用 *foo() 生成器时,现在 yield 委托到了它的迭代器。一旦迭代器控制消耗了整个 *foo() 迭代器就会自动转回控制 *bar()

实际上,yield 委托甚至并不要求必须转到另一个生成器,它可以转到一个非生成器的一般 iterable。比如:

function* bar() {
  console.log('inside *bar():', yield 'A')
  // yield 委托给非生成器
  console.log('inside *bar():', yield* ['B', 'C', 'D'])
  console.log('inside *bar():', yield 'E')
  return 'F'
}
var it = bar(),
  i = 0,
  next

while (!(next = it.next(i++)).done) {
  console.log('outside:', next.value)
}
console.log('outside:', next.value)

这里没有为迭代器没有显式的返回值,所以 yield* 表达式完成后得到的是一个 undefined

另外,和 yield 委托透明地双向传递消息的方式一样,错误和异常也是双向传递的。

当然,yield 委托可以跟踪任意多委托步骤,只要你把它们连在一起。甚至可以使用 yield 委托实现异步的生成器递归,即一个 yield 委托到它自身的生成器:

function* foo(val) {
  if (val > 1) {
    // 生成器递归
    val = yield* foo(val - 1)
  }
  return yield request('http://some.url/?v=' + val)
}
function* bar() {
  var r1 = yield* foo(3)
  console.log(r1)
}

run(bar)

# 生成器并发

两个同时运行的进程可以合作式地交替运作,而很多时候这可以产生(双关,原文为 yield:既指产生又指 yield 关键字)非常强大的异步表示:其中两个不同并发 Ajax 响应处理函数需要彼此协调,以确保数据交流不会出现竞态条件。

// request(..) 是一个支持 Promise 的 Ajax 工具
var res = []
function* reqData(url) {
  var data = yield request(url)
  // 控制转移
  yield
  res.push(data)
}
var it1 = reqData('http://some.url.1')
var it2 = reqData('http://some.url.2')
var p1 = it.next()
var p2 = it.next()
p1.then(function(data) {
  it1.next(data)
})
p2.then(function(data) {
  it2.next(data)
})
Promise.all([p1, p2]).then(function() {
  it1.next()
  it2.next()
})

现在 *reqData(..) 的两个实例确实是并发运行了,而且(至少对于前一部分来说)是相互独立的。而且还可以做的更好,设想一下创建一个称为 runAll(..) 的工具:

var res = []
runAll(
  function*() {
    var p1 = request('http://some.url.1')
    // 控制转移
    yield
    res.push(yield p1)
  },
  function*() {
    var p2 = request('http://some.url.2')
    // 控制转移
    yield
    res.push(yield p2)
  },
)

# 形实转换程序

有一个早期的前 JavaScript 概念,称为形实转换程序(thunk):指一个用于调用另外一个函数的函数,没有任何参数。

简而言之,你用一个函数定义封装函数调用,包括需要的任何参数,来定义这个调用的执行,那么这个封装函数就是一个形实转换程序。之后在执行这个 thunk 时,最终就是调用了原始的函数。

function foo(x, y) {
  return x + y
}
function fooThunk() {
  return foo(3, 4)
}
// 将来
console.log(fooThunk()) // 7

但如果是异步的 thunk 呢?我们可以把这个狭窄的 thunk 定义扩展到包含让它接收一个回调。

function foo(x, y, cb) {
  setTimeout(function() {
    cb(x + y)
  }, 1000)
}
function fooThunk(cb) {
  foo(3, 4, cb)
}
// 将来
fooThunk(function(sum) {
  console.log(sum) // 7
})

你并不会想手工编写 thunk。所以,我们发明一个工具来做这部分封装工作:

function thunkify(fn) {
  var args = [].slice.call(arguments, 1)
  return function(cb) {
    args.push(cb)
    return fn.apply(null, args)
  }
}
var fooThunk = thunkify(foo, 3, 4)
// 将来
fooThunk(function(sum) {
  console.log(sum) // 7
})

事实上,在 JavaScript 中使用 thunk 的典型方案是由 thunkify(..) 工具产生一个生成 thunk 的函数:

function thunkify(fn) {
  return function() {
    var args = [].slice.call(arguments)
    return function(cb) {
      args.push(cb)
      return fn.apply(null, args)
    }
  }
}

这和 promisify(..) 很类似,所以可以 yield 出 Promise 以获得异步性的生成器,也可以为异步性而 yield thunk。为此我们所需要的只是一个更智能的 run(..) 工具能够向 yield 出来的 thunk 提供回调:

// ..
// 我们收到返回的 thunk 了吗?
else if (typeof next.value == "function") {
  return new Promise(function (resolve, reject) {
    // 用 error-first 回调调用这个 thunk
    next.value(function (err, msg) {
      if (err) {
        reject(err);
      } else {
        resolve(msg);
      }
    });
  }).then(handleNext, function handleErr(err) {
    return Promise.resolve(it.throw(err)).then(handleResult);
  });
}

# 参考