生成器(Generator)

生成器(Generator)能够说是在 ES2015 中最为强悍的一个新特性,由于生成器是涉及到 ECMAScript 引擎运行底层的特性,生成器能够实现一些从前没法想象的事情。javascript

来龙

生成器第一次出如今 CLU1 语言中,这门语言是由 MIT (美国麻省理工大学)的 Barbara Liskov 教授和她的学生们在 1974 年至 1975 年所设计和开发出来的。这门语言虽然古老,可是却提出了不少现在被普遍使用的编程语言特性,而生成器即是其中的一个。html

而在 CLU 语言以后,有 Icon 语言2、Python 语言3、C# 语言4和 Ruby 语言5等都受 CLU 语言影响,实现了生成器的特性。在 CLU 语言和 C# 语言中,生成器被称为迭代器(Iterator),而在 Ruby 语言中称为枚举器(Enumerator)。java

然而不管它被成为何,所被赋予的能力都是相同的。生成器的主要目的是用于经过一段程序,来持续被迭代或枚举出符合某个公式或算法的有序数列中的元素,而这个程序即是用于实现这个公式或算法,而不须要将目标数列完整写出。python

咱们来举一个简单的例子,斐波那契数列是很是著名一个理论数学基础数列。它的前两项是 0 和 1,从第三项开始全部的元素都遵循这样的一条公式:算法

那么,依靠程序咱们能够这样实现:编程

const fibonacci = [ 0, 1 ]
const n = 10

for (let i = 2; i < n - 1; ++i) {
  fibonacci.push(fibonacci[i - 1] + fibonacci[i - 2])
}
console.log(fibonacci) //=> [0, 1, 1, 2, 3, 5, 8, 13, 21]

可是这种须要肯定一个数量来取得相应的数列,但若须要按需获取元素,那就可使用生成器来实现了。数组

function* fibo() {
  let a = 0
  let b = 1

  yield a
  yield b

  while (true) {
    let next = a + b
    a = b
    b = next
    yield next
  }
}

let generator = fibo()

for (var i = 0; i < 10; i++)
  console.log(generator.next().value) //=> 0 1 1 2 3 5 8 13 21 34 55

你必定会对这段代码感到很奇怪:为何 function 语句后会有一个 *?为何函数里使用了 while (true) 却没有由于进入死循环而致使程序卡死?而这个 yield 又是什么语句?k4ruby

没必要着急,咱们一一道来。异步

基本概念

生成器是 ES2015 中同时包含语法和底层支持的一个新特性,其中有几个相关的概念是须要先了解的。编程语言

生成器函数(Generator Function)

生成器函数是 ES2015 中生成器的最主要表现方式,它与普通的函数语法差异在于,在 function 语句以后和函数名以前,有一个 * 做为它是一个生成器函数的标示符。

function* fibo() {
  // ...
}

生成器函数的定义并非强制性使用声明式的,与普通函数同样可使用定义式进行定义。

const fnName = function*() { /* ... */ }

生成器函数的函数体内容将会是所生成的生成器的执行内容,在这些内容之中,yield 语句的引入使得生成器函数与普通函数有了区别。yield 语句的做用与 return 语句有些类似,但 yield 语句的做用并不是退出函数体,而是切出当前函数的运行时(此处为一个类协程,Semi-coroutine),并与此同时能够讲一个值(能够是任何类型)带到主线程中。

咱们以一个比较形象的例子来作比喻,你能够把整个生成器运行时当作一条长长的瑞士卷(while (true) 则就是无限长的),ECMAScript 引擎在每一次遇到 yield 就要切一刀,而切面所成的“纹路”则是 yield 出来的值。

Swiss Roll

生成器(Generator)

从计算机科学角度上看,生成器是一种类协程或半协程(Semi-coroutine),生成器提供了一种能够经过特定语句或方法来使生成器的执行对象(Execution)暂停,而这语句通常都是 yield。上面的斐波那契数列的生成器即是经过 yield 语句将每一次的公式计算结果切出执行对象,并带到主线程上来。

在 ES2015 中,yield 能够将一个值带出协程,而主线程也能够经过生成器对象的方法将一个值带回生成器的执行对象中去。

const inputValue = yield outputValue

生成器切出执行对象并带出 outputValue,主线程通过同步或异步的处理后,经过 .next(val) 方法将 inputValue 带回生成器的执行对象中。

使用方法

在了解了生成器的背景知识后,咱们就能够开始来看看在 ES2015 中,咱们要如何使用这个新特性。

构建生成器函数

使用生成器的第一步天然是要构建一个生成器函数,以生成相对应的生成器对象。假设咱们须要按照下面这个公式来生成一个数列,并以生成器做为构建基础。(此处咱们暂不做公式化简)

为了使得生成器可以不断根据公式输出数列元素,咱们与上面的斐波那契数列实例同样,使用 while (true) 循环以保持程序的不断执行。

function* genFn() {
  let a = 2
  
  yield a
  
  while (true) {
    yield a = a / (2 * a + 1)
  }
}

在定义首项为 2 以后,首先将首项经过 yield 做为第一个值切出,其后经过循环和公式将每一项输出。

启动生成器

生成器函数不能直接做为函数来使用,执行生成器函数会返回一个生成器对象,将用于运行生成器内容和接受其中的值。

const gen = genFn()

生成器是是经过生成器函数的一个生成器(类)实例,咱们能够简单地用一段伪代码来讲明生成器这个类的基本内容和用法。

class Generator {
  next(value)
  throw(error)
  [@iterator]()
}
操做方法(语法) 方法内容
generator.next(value) 获取下一个生成器切出状态。(第一次执行时为第一个切出状态)。
generator.throw(error) 向当前生成器执行对象抛出一个错误,并终止生成器的运行。
generator[@iterator] @iteratorSymbol.iterator,为生成器提供实现可迭代对象的方法。使其能够直接被 for...of 循环语句直接使用。

其中 .next(value) 方法会返回一个状态对象,其中包含当前生成器的运行状态和所返回的值。

{
  value: Any,
  done: Boolean
}

生成器执行对象会不断检查生成器的状态,一旦遇到生成器内的最后一个 yield 语句或第一个 return 语句时,生成器便进入终止状态,即状态对象中的 done 属性会从 false 变为 true

.throw(error) 方法会提早让生成器进入终止状态,并将 error 做为错误抛出。

运行生成器内容

由于生成器对象自身也是一种可迭代对象,因此咱们直接使用 for...of 循环将其中输出的值打印出来。

for (const a of gen) {
  if (a < 1/100) break
    
  console.log(a)
}
//=>
//  2
//  0.4
//  0.2222222222
//  ...

深刻理解

运行模式

为了能更好地理解生成器内部的运行模式,咱们将上面的这个例子以流程图的形式展现出来。

图解 Generator

生成器是一种能够被暂停的运行时,在这个例子中,每一次 yield 都会将当前生成器执行对象暂停并输出一个值到主线程。而这在生成器内部的代码是不须要作过多体现的,只须要清楚 yield 语句是暂停的标志及其做用便可。

生成器函数以及生成器对象的检测

事实上 ES2015 的生成器函数也是一种构造函数或类,开发者定义的每个生成器函数均可以看作对应生成器的类,而所产生的生成器都是这些类的派生实例。

在不少基于类(或原型)的库中,咱们能够常常看到这样的代码。

function Point(x, y) {
  if (!(this instanceof Point)) return new Point(x, y)
  // ...
}

const p1 = new Point(1, 2)
const p2 = Point(2, 3)

这一句代码的做用是为了不开发者在建立某一个类的实例时,没有使用 new 语句而致使的错误。而 ECMAScript 內部中的绝大部分类型构造函数(不包括 MapSet 及他们的 Weak 版本)都带有这种特性。

String()  //=> ""
Number()  //=> 0
Boolean() //=> false
Object()  //=> Object {}
Array()   //=> []
Date()    //=> the current time
RegExp()  //=> /(?:)/

TIPS: 在代码风格检查工具 ESLint 中有一个可选特性名为 no-new 即相比使用 new,更倾向于使用直接调用构造函数来建立实例。

那么一样的,生成器函数也支持这种特性,而在互联网上的大多数文献都使用了直接执行的方法建立生成器实例。若是咱们尝试嗅探生成器函数和生成器实例的原型,咱们能够到这样的信息。

function* genFn() {}
const gen = genFn()

console.log(genFn.constructor.prototype) //=> GeneratorFunction
console.log(gen.constructor.prototype)   //=> Generator

这样咱们即可知,咱们能够经过使用 instanceof 语句来得知一个生成器实例是否为一个生成器函数所对应的实例。

console.log(gen instanceof genFn) //=> true

十分惋惜的是,目前原生支持生成器的主流 JavaScript 引擎(如 Google V八、Mozilla SpiderMonkey)并无将 GeneratorFunctionGenerator 类暴露出来。这就意味着没办法简单地使用 instanceof 来断定一个对象是不是生成器函数或生成器实例。但若是你确实但愿对一个未知的对象检测它是不是一个生成器函数或者生成器实例,也能够经过一些取巧的办法来实现。

对于原生支持生成器的运行环境来讲,生成器函数自身带有一个 constructor 属性指向并无被暴露出来的 GeneratorFunction。那么咱们就能够利用一个咱们已知的生成器函数的 constructor 来检验一个函数是不是生成器函数。

function isGeneratorFunction(fn) {
  const genFn = (function*(){}).constructor

  return fn instanceof genFn
}

function* genFn() {
  let a = 2
  
  yield a
  
  while (true) {
    yield a = a / (2 * a + 1)
  }
}

console.log(isGeneratorFunction(genFn)) //=> true

显然出于性能考虑,咱们能够将这个断定函数利用惰性加载进行优化。

function isGeneratorFunction(fn) {
  const genFn = (function*(){}).constructor

  return (isGeneratorFunction = fn => fn instanceof genFn)(fn)
}

相对于生成器函数,生成器实例的检测就更为困难。由于没法经过对已知生成器实例自身的属性来获取被运行引擎所隐藏起来的 Generator 构造函数,因此没法直接用 instanceof 语句来进行类型检测。也就是说咱们须要利用别的方法来实现这个需求。

在上一个章节中,咱们介绍到了在 ECMAScript 中,每个对象都会有一个 toString() 方法的实现以及其中一部分有 Symbol.toStringTag 做为属性键的属性,以用于输出一个为了填补引用对象没法被直接序列化的字符串。而这个字符串是能够间接地探测出这个对象的构造函数名称,即带有直接关系的类。

那么对于生成器对象来讲,与它拥有直接关系的类除了其对应的生成器函数之外,即是被隐藏起来的 Generator 类了。而生成器对象的 @@toStringTag 属性正正也是 Generator,这样的话咱们就有了实现的思路了。在著名的 JavaScript 工具类库 LoDash6 的类型检测中,正式使用了(包括但不限于)这种方法来对未知对象进行类型检查,而咱们也能够试着使用这种手段。

function isGenerator(obj) {
  return obj.toString ? obj.toString() === '[object Generator]' : false
}

function* genFn() {}
const gen = genFn()

console.log(isGenerator(gen)) //=> true
console.log(isGenerator({}))  //=> false

而另一方面,咱们既然已经知道了生成器实例一定带有 @@toStringTag 属性并其值夜一定为 Generator,咱们也能够经过这个来检测位置对象是否为生成器实例。

function isGenerator(obj) {
  return obj[Symbol && Symbol.toStringTag ? Symbol.toStringTag : false] === 'Generator'
}

console.log(isGenerator(gen)) //=> true
console.log(isGenerator({}))  //=> false

此处为了防止由于运行环境不支持 Symbol@@toStringTag 而致使报错,须要使用先作兼容性检测以完成兼容降级。

而咱们再回过头来看看生成器函数,咱们是否也可使用 @@toStringTag 属性来对生成器函数进行类型检测呢?咱们在一个同时支持生成器和 @@toStringTag 的运行环境中运行下面这段代码。

function* genFn() {}

console.log(genFn[Symbol.toStringTag]) //=> GeneratorFunction

这显然是可行的,那么咱们就来为前面的 isGeneratorFunction 方法再进行优化。

function isGeneratorFunction(fn) {
  return fn[Symbol && Symbol.toStringTag ? Symbol.toStringTag : false] === 'GeneratorFunction'
}

console.log(isGeneratorFunction(genFn)) //=> true

而当运行环境不支持 @@toStringTag 时也能够经过 instanceof 语句来进行检测。

function isGeneratorFunction(fn) {
  // If the current engine supports Symbol and @@toStringTag
  if (Symbol && Symbol.toStringTag) {
    return (isGeneratorFunction = fn => fn[Symbol.toStringTag] === 'GeneratorFunction')(fn)
  }

  // Using instanceof statement for detecting
  const genFn = (function*(){}).constructor

  return (isGeneratorFunction = fn => fn instanceof genFn)(fn)
}

console.log(isGeneratorFunction(genFn)) //=> true

生成器嵌套

虽说到如今为止,咱们所举出的生成器例子都是单一辈子成器进行使用。可是在实际开发中,咱们一样会遇到须要一个生成器嵌套在另外一个生成器内的状况,就好比数学中的分段函数或嵌套的数组公式等。

咱们假设有这样的一个分段函数,咱们须要对其进行积分计算。

分别对分段函数的各分段做积分,以便编写程序进行积分。

此处咱们能够分别对分段函数的两个部分分别创建生成器函数并使用牛顿-科特斯公式(Newton-Cotes formulas)7来进行积分计算。

// Newton-Cotes formulas
function* newton_cotes(f, a, b, n) {
  const gaps = (b - a) / n
  const h = gaps / 2

  for (var i = 0; i < n; i++) {
    yield h / 45 *
      (7 * f(a + i * gaps) +
      32 * f(a + i * gaps + 0.25 * gaps) +
      12 * f(a + i * gaps + 0.5 * gaps) +
      32 * f(a + i * gaps + 0.75 * gaps) +
      7 * f(a + (i + 1) * gaps))
  }
}

在编写两个分段部分的生成器以前,咱们须要先引入一个新语法 yield*。它与 yield 的区别在于,yield* 的功能是为了将一个生成器对象嵌套于另外一个生成器内,并将其展开。咱们以一个简单地例子说明。

function* foo() {
  yield 1
  yield 2
}

function* bar() {
  yield* foo()
  yield 3
  yield 4
}

for (const n of bar()) console.log(n)
//=>
//  1
//  2
//  3
//  4

利用 yield* 语句咱们就能够将生成器进行嵌套和组合,使得不一样的生成器所输出的值能够被同一个生成器连续输出。

function* Part1(n) {
  yield* newton_cotes(x => Math.pow(x, 2), -2, 0, n)
}

function* Part2(n) {
  yield* newton_cotes(x => Math.sin(x), 0, 2, n)
}

function* sum() {
  const n = 100

  yield* Part1(n)
  yield* Part2(n)
}

最终咱们将 sum() 生成器的全部输出值相加便可。

生成器 ≈ 协程?

从运行机制的角度上看,生成器拥有暂停运行时的能力,那么生成器的运用是否只仅限于生成数据呢?在上文中,咱们提到了生成器是一种类协程,而协程自身是能够经过生成器的特性来进行模拟呢。

在现代 JavaScript 应用开发中,咱们常常会使用到异步操做(如在 Node.js 开发中绝大部分使用到的 IO 操做都是异步的)。可是当异步操做的层级过深时,就可能会出现回调地狱(Callback Hell)。

io1((err, res1) => {
  io2(res1, (err, res2) => {
    io3(res2, (err, res3) => {
      io4(res3, (err, res4) => {
        io5(res5, (err, res5) => {
          // ......
        })
      })
    })
  })
})

显然这样很不适合真正的复杂开发场景,而咱们究竟要如何对着进行优化呢?咱们知道 yield 语句能够将一个值带出生成器执行环境,而这个值能够是任何类型的值,这就意味着咱们能够利用这一特性作一些更有意思的事情了。

咱们回过头来看看生成器对象的操做方法,生成器执行对象的暂停状态能够用 .next(value) 方法恢复,而这个方法是能够被异步执行的。这就说明若是咱们将异步 IO 的操做经过 yield 语句来从生成器执行对象带到主线程中,在主线程中完成后再经过 .next(value) 方法将执行结果带回到生成器执行对象中,这一流程在生成器的代码中是能够以同步的写法完成的。

具体思路成型后,咱们先以一个简单的例子来实现。为了实现以生成器做为逻辑执行主体,把异步方法带到主线程去,就要先将异步函数作一层包装,使得其能够在带出生成器执行对象以后再执行。

// Before
function echo(content, callback) {
  callback(null, content)
}

// After
function echo(content) {
  return callback => {
    callback(null, content)
  }
}

这样咱们就能够在生成器内使用这个异步方法了。可是还不足够,将方法带出生成器执行对象后,还须要在主线程将带出的函数执行才可实现应有的需求。上面咱们经过封装所获得的异步方法在生成器内部执行后,能够经过 yield 语句将内层的函数带到主线程中。这样咱们就能够在主线程中执行这个函数并获得返回值,而后将其返回到生成器执行对象中。

function run(genFn) {
  const gen = genFn()
  
  const next = value => {
    const ret = gen.next(value)
    if (ret.done) return
    
    ret.value((err, val) => {
      if (err) return console.error(err)
      
      // Looop
      next(val)
    })
  }
  
  // First call
  next()
}

经过这个运行工具,咱们即可以将生成器函数做为逻辑的运行载体,从而将以前多层嵌套的异步操做所有扁平化。

run(function*() {
  const msg1 = yield echo('Hello')
  const msg2 = yield echo(`${msg1} World`)

  console.log(msg2) //=> Hello Wolrd
})

经过简单地封装,咱们已经尝到了一些甜头,那么再进一步加强以后又会有什么有趣的东西呢?Node.js 社区中有一个第三方库名为 co,意为 coroutine,这个库的意义在于利用生成器来模拟协程。而咱们这里介绍的就是其中的一部分,co 的功能则更为丰富,能够直接使用 Promise 封装工具,若是异步方法有自带 Promise 的接口,就无需再次封装。此外 co 还能够直接实现生成器的嵌套调用,也就是说能够经过 co 来实现逻辑代码的所有同步化开发。

import co from 'co'
import { promisify } from 'bluebird'
import fs from 'fs'
import path from 'path'
  
const filepath = path.resolve(process.cwd(), './data.txt')
const defaultData = new Buffer('Hello World')

co(function*() {
  const exists = yield promisify(fs.exists(filepath))

  if (exists) {
    const data = yield promisify(fs.readFile(filepath))
    // ...
  } else {
    yield promisify(fs.writeFile(filepath, defaultData))
    // ...
  }
})

Reference

[1] CLU Language http://www.pmg.lcs.mit.edu/CLU.html
[2] Icon Language http://www.cs.arizona.edu/icon
[3] Python Language http://www.python.org
[4] C# Language http://msdn.microsoft.com/pt-br/vcsharp/default.aspx
[5] Ruby Language http://www.ruby-lang.org
[6] LoDash https://lodash.com
[7] Newton-Cotes formulas https://en.wikipedia.org/wiki/Newton%E2%80%93Cotes_formulas

相关文章
相关标签/搜索