Generator 函数
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 就是一个异步操作的容器。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
1 | function* helloWorldGenerator() { |
调用遍历器对象的next方法,使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。
1 | hw.next() |
yield表达式
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
- yield表达式与 Iterator 接口的关系 *
任意一个对象的Symbol.iterator方法,等于该对象的遍历器生成函数,调用该函数会返回该对象的一个遍历器对象。
1 | var myIterable = {}; |
next 方法的参数
yield表达式本身没有返回值,或者说总是返回undefined。next方法可以带一个参数,该参数就会被当作上一个yield表达式的返回值。
1 | function* f() { |
如果想要第一次调用next方法时,就能够输入值,可以在 Generator 函数外面再包一层。
1 | function wrapper(generatorFunction) { |
上面代码中,Generator 函数如果不用wrapper先包一层,是无法第一次调用next方法,就输入参数的。
for…of
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法。
1 | function* foo() { |
利用 Generator 函数和for…of循环,实现斐波那契数列的例子。
1 | function* fibonacci() { |
yield* 表达式
如果在 Generator 函数内部,调用另一个 Generator 函数。需要在前者的函数体内部,自己手动完成遍历。
1 | function* foo() { |
ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
1 | function* bar() { |
作为对象属性的 Generator 函数
如果一个对象的属性是 Generator 函数,可以简写成下面的形式。
1 | let obj = { |
上面代码中,myGeneratorMethod属性前面有一个星号,表示这个属性是一个 Generator 函数。
它的完整形式如下,与上面的写法是等价的。
1 | let obj = { |
生成一个空对象,使用call方法绑定 Generator 函数内部的this。这样,构造函数调用以后,这个空对象就是 Generator 函数的实例对象了。
1 | function* F() { |
首先是F内部的this对象绑定obj对象,然后调用它,返回一个 Iterator 对象。这个对象执行三次next方法(因为F内部有两个yield表达式),完成 F 内部所有代码的运行。这时,所有内部属性都绑定在obj对象上了,因此obj对象也就成了F的实例。
上面代码中,执行的是遍历器对象f,但是生成的对象实例是obj,有没有办法将这两个对象统一呢?
一个办法就是将obj换成F.prototype。
1 | function* F() { |
再将F改成构造函数,就可以对它执行new命令了。
1 | function* gen() { |
Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景。
(1)异步操作的同步化表达
Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。
1 | function* main() { |
上面代码的main函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall函数中的next方法,必须加上response参数,因为yield表达式,本身是没有值的,总是等于undefined。
下面是另一个例子,通过 Generator 函数逐行读取文本文件。
1 | function* numbers() { |
上面代码打开文本文件,使用yield表达式可以手动逐行读取文件。
(2)控制流管理
如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。
1 | step1(function (value1) { |
采用 Promise 改写上面的代码。
1 | Promise.resolve(step1) |
上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。
1 | function* longRunningTask(value1) { |
函数的异步应用
ES6 诞生以前,异步编程的方法,大概有下面四种。
回调函数 事件监听 发布/订阅 Promise 对象
回调
1 | fs.readFile('/etc/passwd', 'utf-8', function (err, data) { |
Promise
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套。假定读取A文件之后,再读取B文件,代码如下。
1 | fs.readFile(fileA, 'utf-8', function (err, data) { |
Promise 对象就是为了解决这个问题而提出的。它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。采用 Promise,连续读取多个文件,写法如下。
1 | var readFile = require('fs-readfile-promise'); |
使用了fs-readfile-promise模块,它的作用就是返回一个 Promise 版本的readFile函数。Promise 提供then方法加载回调函数,catch方法捕捉执行过程中抛出的错误。
可以看到,Promise 的写法只是回调函数的改进,使用then方法以后,异步任务的两段执行看得更清楚了,除此以外,并无新意。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator 函数的流程管理
1 | function* gen() { |
但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。这时,Thunk 函数就能派上用处。以读取文件为例。下面的 Generator 函数封装了两个异步操作。
1 | var fs = require('fs'); |
上面代码中,yield命令用于将程序的执行权移出 Generator 函数,那么就需要一种方法,将执行权再交还给 Generator 函数。
1 | var g = gen(); |
变量g是 Generator 函数的内部指针,表示目前执行到哪一步。next方法负责将指针移动到下一步,并返回该步的信息(value属性和done属性)。
Thunk 函数的自动流程管理
Thunk 函数真正的威力,在于可以自动执行 Generator 函数。下面就是一个基于 Thunk 函数的 Generator 执行器。
1 | function run(fn) { |
上面代码的run函数,就是一个 Generator 函数的自动执行器。内部的next函数就是 Thunk 的回调函数。next函数先将指针移到 Generator 函数的下一步(gen.next方法),然后判断 Generator 函数是否结束(result.done属性),如果没结束,就将next函数再传入 Thunk 函数(result.value属性),否则就直接退出。
有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。
1 | var g = function* (){ |
上面代码中,函数g封装了n个异步的读取文件操作,只要执行run函数,这些操作就会自动完成。这样一来,异步操作不仅可以写得像同步操作,而且一行代码就可以执行。
Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
co 模块
co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
1 | var gen = function* () { |
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
1 | co(gen).then(function (){ |
** co 模块的原理 **
为什么 co 可以自动执行 Generator 函数?
前面说过,Generator 就是一个异步操作的容器。它的自动执行需要一种机制,当异步操作有了结果,能够自动交回执行权。两种方法可以做到这一点。
(1)回调函数。将异步操作包装成 Thunk 函数,在回调函数里面交回执行权。
(2)Promise 对象。将异步操作包装成 Promise 对象,用then方法交回执行权。
基于 Promise 对象的自动执行器
还是沿用上面的例子。首先,把fs模块的readFile方法包装成一个 Promise 对象。
1 | var fs = require('fs'); |
然后,手动执行上面的 Generator 函数。
1 | var g = gen(); |
手动执行其实就是用then方法,层层添加回调函数。理解了这一点,就可以写出一个自动执行器。
1 | function run(gen){ |
co 模块的源码分析
co 支持并发的异步操作,即允许某些操作同时进行,等到它们全部完成,才进行下一步。
这时,要把并发的操作都放在数组或对象里面,跟在yield语句后面。
1 | // 数组的写法 |
例子。
1 | co(function* () { |
上面的代码允许并发三个somethingAsync异步操作,等到它们全部完成,才会进行下一步。