Generator函数
简介
Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator 函数有多种理解角度。语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。通过调用它的next方法,来达到遍历每个内部状态的效果。
执行 Generator 函数会返回一个遍历器对象,也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数,即部署了 Iterator 接口。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。
Generator函数的基本语法
Generator 函数的标准形式,* 和 yield,这里的yield表示暂缓执行,即执行到这一步就会退出函数,直到调用next方法才会继续往下执行,如此以来就可以把 Generator 函数看作一个状态机,这里封装了 hello world ending 三个状态,通过next方法来依次遍历这三个状态。
每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行,done属性表示是否遍历完成
function* helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
yiled
由于 Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。
遍历器对象的next方法的运行逻辑如下。
(1)遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
(2)下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
(3)如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
(4)如果该函数没有return语句,则返回的对象的value属性值为undefined。
通过 yiled,Generator 函数可以返回无数多个结果,即生成了一系列的值。
需要注意的是,yield表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
yield表达式只能用在 Generator 函数里面,用在其他地方都会报错。
Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f(); // 注意这里 是加了 括号的
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f如果是普通函数,在为变量generator赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用next方法时,函数f才会执行。
另外,yield表达式如果用在另一个表达式之中,必须放在圆括号里面。
yield表达式用作函数参数或放在赋值表达式的右边,可以不加括号。
next
调用next方法,即可让 Generator 函数继续执行下一步操作,下面简单介绍 next 方法
next方法可以带一个参数,该参数会被当做上一个yield表达式的返回值
function* f() {
for(var i = 0; true; i++) {
var reset = yield i;
if(reset) { i = -1; }
}
}
var g = f();
g.next() // { value: 0, done: false }
g.next() // { value: 1, done: false }
g.next(true) // { value: 0, done: false }
注意,由于next方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的。V8 引擎直接忽略第一次使用next方法时的参数,只有从第二次使用next方法开始,参数才是有效的。从语义上讲,第一个next方法用来启动遍历器对象,所以不用带有参数。
与 Iterator 的关系
前面说过,部署了 Symbol.iterator 方法的对象,即部署了 Iterator 接口就能被 for…of 来遍历。即该方法就等于该对象的遍历器生成函数(Generator函数),意思是可以用 Generator 函数来部署对象的 Iterator 接口。
for…of循环
for…of循环可以自动遍历 Generator 函数运行时生成的Iterator对象,且此时不再需要调用next方法
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6; // 这里需要注意,一旦next方法的返回对象的done属性为true,for...of循环就会中止,且不包含该返回对象,所以上面代码的return语句返回的6,不包括在for...of循环之中
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
利用for…of循环,可以写出遍历任意对象(object)的方法。原生的 JavaScript 对象没有遍历接口,无法使用for…of循环,通过 Generator 函数为它加上这个接口,就可以用了。
throw方法
Generator 函数返回的遍历器对象,都有一个throw方法,可以在函数体外抛出错误,然后在 Generator 函数体内捕获。
var g = function* () {
try {
yield;
} catch (e) {
console.log('内部捕获', e);
}
};
var i = g();
i.next();
try {
i.throw('a');
i.throw('b');
} catch (e) {
console.log('外部捕获', e);
}
// 内部捕获 a
// 外部捕获 b
// 第一个错误被 Generator 函数体内的catch语句捕获。i第二次抛出错误,由于 Generator 函数内部的catch语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了 Generator 函数体,被函数体外的catch语句捕获。
注意,不要混淆遍历器对象的throw方法和全局的throw命令。区别 x.throw throw
如果 Generator 函数内部没有部署try...catch代码块,那么throw方法抛出的错误,将被外部try...catch代码块捕获。
如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。
throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
另外,throw命令与g.throw方法是无关的,两者互不影响。
一旦 Generator 执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了
return 方法
Generator 函数返回的遍历器对象,还有一个return()方法,可以返回给定的值,并且终结遍历 Generator 函数。
如果return()方法调用时,不提供参数,则返回值的value属性为undefined。
如果 Generator 函数内部有try...finally代码块,且正在执行try代码块,那么return()方法会导致立刻进入finally代码块,执行完以后,整个函数才会结束。
next() throw() return()
next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式
-
next()
next()是将yield表达式替换成一个值。const g = function* (x, y) { let result = yield x + y; return result; }; const gen = g(1, 2); gen.next(); // Object {value: 3, done: false} gen.next(1); // Object {value: 1, done: true} // 相当于将 let result = yield x + y // 替换成 let result = 1; -
throw()
throw()是将yield表达式替换成一个throw语句gen.throw(new Error('出错了')); // Uncaught Error: 出错了 // 相当于将 let result = yield x + y // 替换成 let result = throw(new Error('出错了')); -
return()
return()是将yield表达式替换成一个return语句。gen.return(2); // Object {value: 2, done: true} // 相当于将 let result = yield x + y // 替换成 let result = return 2;
yield *表达式
ES6 提供了yield*表达式,作为解决办法,用来在一个 Generator 函数里面执行另一个 Generator 函数。
从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield*表达式。yield*后面可以跟只要部署了 Iterator 接口的数据结构!
function* foo() {
yield 'a';
yield 'b';
}
function* bar() {
yield 'x';
yield* foo();
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
yield 'a';
yield 'b';
yield 'y';
}
// 等同于
function* bar() {
yield 'x';
for (let v of foo()) {
yield v;
}
yield 'y';
}
for (let v of bar()){
console.log(v);
}
// "x"
// "a"
// "b"
// "y"
使用yield*语句遍历完全二叉树。
// 下面是二叉树的构造函数,
// 三个参数分别是左树、当前节点和右树
function Tree(left, label, right) {
this.left = left;
this.label = label;
this.right = right;
}
// 下面是中序(inorder)遍历函数。
// 由于返回的是一个遍历器,所以要用generator函数。
// 函数体内采用递归算法,所以左树和右树要用yield*遍历
function* inorder(t) {
if (t) {
yield* inorder(t.left);
yield t.label;
yield* inorder(t.right);
}
}
// 下面生成二叉树
function make(array) {
// 判断是否为叶节点
if (array.length == 1) return new Tree(null, array[0], null);
return new Tree(make(array[0]), array[1], make(array[2]));
}
let tree = make([[['a'], 'b', ['c']], 'd', [['e'], 'f', ['g']]]);
// 遍历二叉树
var result = [];
for (let node of inorder(tree)) {
result.push(node);
}
result
// ['a', 'b', 'c', 'd', 'e', 'f', 'g']
作为对象属性的 Generator 函数
let obj = {
* myGeneratorMethod() {
···
}
};
// 等价于
let obj = {
myGeneratorMethod: function* () {
// ···
}
};
Generator 函数 的 this
Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator 函数的prototype对象上的方法。
function* g() {}
g.prototype.hello = function () {
return 'hi!';
};
let obj = g();
obj instanceof g // true
obj.hello() // 'hi!'
function* g() {
this.a = 11;
}
let obj = g();
obj.next();
obj.a // undefined
上面代码表明,Generator 函数g返回的遍历器obj,是g的实例,而且继承了g.prototype。但是,如果把g当作普通的构造函数,并不会生效,因为g返回的总是遍历器对象,而不是this对象。
所以这里Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性,因为返回的是一个新的遍历器对象。
Generator 函数也不能跟new命令一起用,会报错
让 Generator 函数返回一个正常的对象实例,既可以用next方法,又可以获得正常的this
-
使用 call 绑定 Generator 函数内部 this
function* F() { this.a = 1; yield this.b = 2; yield this.c = 3; } var obj = {}; var f = F.call(obj); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} obj.a // 1 obj.b // 2 obj.c // 3 -
将 obj 换成 Generator 的 prototype
function* gen() { this.a = 1; yield this.b = 2; yield this.c = 3; } function F() { return gen.call(gen.prototype); } var f = new F(); f.next(); // Object {value: 2, done: false} f.next(); // Object {value: 3, done: false} f.next(); // Object {value: undefined, done: true} f.a // 1 f.b // 2 f.c // 3
Generator 函数的异步应用
异步
所谓"异步",简单说就是一个任务不是连续完成的,可以理解成该任务被人为分成两段,先执行第一段,然后转而执行其他任务,等做好了准备,再回过头执行第二段。
从而达到节省时间。比如煮饭需要30min,如果是同步则需要在电饭煲前干等30min,如果是异步就可以去干其他的事情,等煮饭的结果产生了再来继续进行吃饭的操作。
ES6之前的异步代码
- 回调函数
JavaScript 语言对异步编程的实现,就是回调函数。所谓回调函数,就是把任务的第二段单独写在一个函数里面,等到重新执行这个任务的时候,就直接调用这个函数。
回调函数本身并没有问题,它的问题出现在多个回调函数嵌套,即回调地狱 - 事件监听
- 发布/订阅
- Promise 对象
为了解决回调地狱,Promise对象出现了,它不是新的语法功能,而是一种新的写法,允许将回调函数的嵌套,改成链式调用。
Promise 的最大问题是代码冗余,原来的任务被 Promise 包装了一下,不管什么操作,一眼看去都是一堆then,原来的语义变得很不清楚。
Generator 和 协程
协程
-
传统的编程语言,早有异步编程的解决方案(其实是多任务的解决方案)。其中有一种叫做"协程"(coroutine),意思是多个线程互相协作,完成异步任务。
-
协程有点像函数,又有点像线程。它的运行流程大致如下。
- 第一步,协程
A开始执行。 - 第二步,协程
A执行到一半,进入暂停,执行权转移到协程B。 - 第三步,(一段时间后)协程
B交还执行权。 - 第四步,协程
A恢复执行。
function* asyncJob() { // ...其他代码 var f = yield readFile(fileA); // ...其他代码 }上面代码的函数
asyncJob是一个协程,它的奥妙就在其中的yield命令。它表示执行到此处,执行权将交给其他协程。也就是说,yield命令是异步两个阶段的分界线。协程遇到
yield命令就暂停,等到执行权返回,再从暂停的地方继续往后执行。它的最大优点,就是代码的写法非常像同步操作,如果去除yield命令,简直一模一样。意思就是 使用 generator 可以完美模拟 协程的方式来解决异步操作
- 第一步,协程
协程的 Generator 函数实现
- Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行)。
- 整个 Generator 函数就是一个封装的异步任务,或者说是异步任务的容器。异步操作需要暂停的地方,都用
yield语句注明。
Generator 函数的数据交换和错误处理
- Generator 函数可以暂停执行和恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换和错误处理机制。
next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。- 具体情况可以参见上一节 的 next() throw()
异步任务的封装
var fetch = require('node-fetch');
function* gen(){
var url = 'https://api.github.com/users/github';
var result = yield fetch(url);
console.log(result.bio);
}
// 封装
var g = gen();
var result = g.next();
result.value.then(function(data){
return data.json();
}).then(function(data){
g.next(data);
});
// 执行
Thunk 函数
Thunk 函数是自动执行 Generator 函数的一种方法。
参数的求值策略
- 传值调用
- 传名调用(惰性求值?)
含义
- 编译器的“传名调用”实现,往往是将参数放到一个临时函数之中,再将这个临时函数传入函数体。这个临时函数就叫做 Thunk 函数。
- 它是“传名调用”的一种实现策略,用来替换某个表达式。
js中的 Thunk 函数
-
JavaScript 语言是传值调用,它的 Thunk 函数含义有所不同。在 JavaScript 语言中,Thunk 函数替换的不是表达式,而是多参数函数,将其替换成一个只接受回调函数作为参数的单参数函数。
// 正常版本的readFile(多参数版本) fs.readFile(fileName, callback); // Thunk版本的readFile(单参数版本) var Thunk = function (fileName) { return function (callback) { return fs.readFile(fileName, callback); }; }; var readFileThunk = Thunk(fileName); readFileThunk(callback); -
任何函数,只要参数有回调函数,就能写成 Thunk 函数的形式。下面是一个简单的 Thunk 函数转换器。
// ES5版本 var Thunk = function(fn){ return function (){ var args = Array.prototype.slice.call(arguments); return function (callback){ args.push(callback); return fn.apply(this, args); } }; }; // ES6版本 const Thunk = function(fn) { return function (...args) { return function (callback) { return fn.call(this, ...args, callback); } }; }; -
Thunkify 模块
生产环境的转换器,建议使用 Thunkify 模块
Generator 函数的流程管理
-
Thunk 函数有什么用?回答是以前确实没什么用,但是 ES6 有了 Generator 函数,Thunk 函数现在可以用于 Generator 函数的自动流程管理。
Generator 函数可以自动执行。
function* gen() { // ... } var g = gen(); var res = g.next(); while(!res.done){ console.log(res.value); res = g.next(); } // 上面代码中,Generator 函数gen会自动执行完所有步骤。 // 但是,这不适合异步操作。如果必须保证前一步执行完,才能执行后一步,上面的自动执行就不可行。 var g = gen(); var r1 = g.next(); r1.value(function (err, data) { if (err) throw err; var r2 = g.next(data); r2.value(function (err, data) { if (err) throw err; g.next(data); }); }); // 为了便于理解,我们先看如何手动执行上面这个 Generator 函数。 // 仔细查看上面的代码,可以发现 Generator 函数的执行过程,其实是将同一个回调函数,反复传入next方法的value属性。这使得我们可以用递归来自动完成这个过程 -
下面就是一个基于 Thunk 函数的 Generator 执行器。
function run(fn) { var gen = fn(); function next(err, data) { var result = gen.next(data); if (result.done) return; result.value(next); } next(); } function* g() { // ... } run(g);有了这个执行器,执行 Generator 函数方便多了。不管内部有多少个异步操作,直接把 Generator 函数传入
run函数即可。当然,前提是每一个异步操作,都要是 Thunk 函数,也就是说,跟在yield命令后面的必须是 Thunk 函数。Thunk 函数并不是 Generator 函数自动执行的唯一方案。因为自动执行的关键是,必须有一种机制,自动控制 Generator 函数的流程,接收和交还程序的执行权。回调函数可以做到这一点,Promise 对象也可以做到这一点。
co 模块
co 模块是著名程序员 TJ Holowaychuk 于 2013 年 6 月发布的一个小工具,用于 Generator 函数的自动执行。
co 模块可以让你不用编写 Generator 函数的执行器。
co函数返回一个Promise对象,因此可以用then方法添加回调函数。
总结
本文主要介绍了 Generator 函数这一基本概念,基本语法,以及它在异步中的表现。
528

被折叠的 条评论
为什么被折叠?



