基本概念
Generator函数是ES6提供的一种异步编程解决方案,语法行为与传统函数完全不同。
Generator函数有多种理解角度。语法上,首先可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
形式上,Generator函数是一个普通函数,但是有两个特征。
- function
关键字与函数名之间有一个星号
- 函数体内部使用yield
表达式,定义不同的内部状态
function*helloWorldGenerator() {
yield 'hello';
yield 'world';
return 'ending';
}
var hw = helloWorldGenerator();
上面代码定义了一个Generator函数helloWorldGenerator
,它内部有两个yield
表达式(hello
和world
),即该函数有三个状态:hello
,world
和return
语句(结束执行)。
Generator函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用Generator函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,也就是上一章介绍的遍历器对象(Iterator Object)。
下一步,必须调用遍历器对象的next
方法,使得指针移向下一个状态,直到遇到下一个yield
表达式(或return
语句)。换言之,Generator函数是分段执行的,yield
表达式是暂停执行的标记,而next
方法可以恢复执行。
遍历器对象的next
方法的运行逻辑如下。
- 遇到
yield
表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。 - 下一次调用
next
方法时,再继续往下执行,直到遇到下一个yield
表达式。 - 如果没有再遇到新的
yield
表达式,就一直运行到函数结束,直到return
语句为止,并将return语句后面的表达式的值,作为返回的对象的value
属性值。 - 如果该函数没有
return
语句,则返回的对象的value
属性值为undefined
。
hw.next()
// { value: 'hello', done: false }
hw.next()
// { value: 'world', done: false }
hw.next()
// { value: 'ending', done: true }
hw.next()
// { value: undefined, done: true }
yield
表达式
由于Generator函数返回的遍历器对象,只有调用next
方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield
表达式就是暂停标志。
需要注意的是,yield
表达式后面的表达式,只有当调用next
方法、内部指针指向该语句时才会执行,因此等于为JavaScript提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。
function* gen() {
yield 123 + 456;
}
Generator函数可以不用yield
表达式,这时就变成了一个单纯的暂缓执行函数。
function* f() {
console.log('执行了!')
}
var generator = f();
setTimeout(function () {
generator.next()
}, 2000);
上面代码中,函数f如果是普通函数,在为变量generator
赋值时就会执行。但是,函数f是一个Generator
函数,就变成只有调用next
方法时,函数f才会执行。
注意,yield
表达式只能用在Generator函数里面,用在其他地方都会报错。
var arr = [1, [[2, 3], 4], [5, 6]];
var flat = function* (a) {
a.forEach(function (item) {
if (typeof item !== 'number') {
yield* flat(item);
} else {
yield item;
}
});
};
for (var f of flat(arr)){
console.log(f);
}
上面代码也会产生句法错误,因为forEach
方法的参数是一个普通函数,但是在里面使用了yield表达式。可以改用for循环。
另外,yield
表达式如果用在另一个表达式之中,必须放在圆括号里面。
console.log('Hello' + (yield)); // OK
console.log('Hello' + (yield 123)); // OK
与Iterator接口的关系
由于Generator函数就是遍历器生成函数,因此可以把Generator赋值给对象的Symbol.iterator
属性,从而使得该对象具有Iterator接口。
let obj = {};
obj[Symbol.iterator] = function * () {
yield 11;
yield 22;
return 33;
};
for (let val of obj) {
console.log(val)
}
也可以在Object原型上增加
Object.prototype[Symbol.iterator] = function* () {
let keys = Object.keys(this);
for(let key of keys){
yield [key, this[key]]
}
}
let fff = {a:1,b:2}
for(let o of fff){
console.log(o)
}
// ["a", 1]
// ["b", 2]
next()
方法的参数
yield
表达式本身没有返回值,或者说总是返回undefined
。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 }
这个功能有很重要的语法意义。Generator函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过next
方法的参数,就有办法在Generator函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为。
function* foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var a = foo(5);
a.next() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:true}
var b = foo(5);
b.next() // { value:6, done:false }
b.next(12) // { value:8, done:false }
b.next(13) // { value:42, done:true }
注意,由于next方法的参数表示上一个yield
表达式的返回值,所以在第一次使用next
方法时,传递参数是无效的。从语义上讲,第一个next
方法用来启动遍历器对象,所以不用带有参数。
再看一个通过next
方法的参数,向Generator函数内部输入值的例子。
function* dataConsumer() {
console.log('Started');
console.log(`1. ${yield}`);
console.log(`2. ${yield}`);
return 'result';
}
let genObj = dataConsumer();
genObj.next();
// Started
genObj.next('a')
// 1. a
genObj.next('b')
// 2. b
上面的代码这样理解:
1. 第一次调用next
方法时,执行了console.log('Started');
,遇到第二句包含yield
语句时暂停了执行,此时并没有执行1
的log
行为,并且此时因为是首次执行,并且yiled后面没有任何返回值,所以返回值是{value: undefined, done: false}
2. 第二次调用next
方法,代码继续执行,执行了1
的log
,这是打印的${yield}
是第一次调用后的结果,接受的是本次next
传入值,所以打印1. a
,这时候语句执行到第三句停了下来,同样由于yield
没有返回值,所以返回值是{value: undefined, done: false}
3. 继续反复执行
改写一下可能更好理解:
function * dataConsumer() {
console.log('Started');
let a = yield;
console.log(`1.${a}`);
let b = yield;
console.log(`2.${b}`);
return 'result';
}
for...of
循环
for...of
循环可以自动遍历Generator函数时生成的Iterator对象,且此时不再需要调用next
方法。
function* foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (let v of foo()) {
console.log(v);
}
// 1 2 3 4 5
这里需要注意,一旦next
方法的返回对象的done
属性为true
,for...of
循环就会中止,且不包含该返回对象,所以上面代码的return
语句返回的6
,不包括在for...of
循环之中。
Generator.prototype.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
上面代码中,遍历器对象i
连续抛出两个错误。第一个错误被Generator函数体内的catch
语句捕获。i第二次抛出错误,由于Generator函数内部的catch
语句已经执行过了,不会再捕捉到这个错误了,所以这个错误就被抛出了Generator函数体,被函数体外的catch
语句捕获。
如果Generator函数内部和外部,都没有部署try...catch
代码块,那么程序将报错,直接中断执行。
var gen = function* gen(){
yield console.log('hello');
yield console.log('world');
}
var g = gen();
g.next();
g.throw();
// hello
// Uncaught undefined
throw
方法被捕获以后,会附带执行下一条yield
表达式。也就是说,会附带执行一次next
方法。
let gen = function * gen() {
try {
yield console.log('hello');
} catch (e) {
console.log(e)
}
yield console.log('world');
};
let g = gen();
g.next();
g.throw ();
// hello
// undefined
// world
这种函数体内捕获错误的机制,大大方便了对错误的处理。多个yield
表达式,可以只用一个try...catch
代码块来捕获错误。如果使用回调函数的写法,想要捕获多个错误,就不得不为每个函数内部写一个错误处理语句,现在只在Generator函数内部写一次catch
语句就可以了。
一旦Generator执行过程中抛出错误,且没有被内部捕获,就不会再执行下去了。如果此后还调用next
方法,将返回一个value
属性等于undefined
、done
属性等于true
的对象,即JavaScript引擎认为这个Generator已经运行结束了。
Generator.prototype.return()
Generator函数返回的遍历器对象,还有一个return
方法,可以返回给定的值,并且终结遍历Generator函数。
function* gen() {
yield 1;
yield 2;
yield 3;
}
var g = gen();
g.next() // { value: 1, done: false }
g.return('foo') // { value: "foo", done: true }
g.next() // { value: undefined, done: true }
如果
return
方法调用时,不提供参数,则返回值的value
属性为undefined
。
如果Generator函数内部有try...finally
代码块,那么return
方法会推迟到finally
代码块执行完再执行。
function* numbers () {
yield 1;
try {
yield 2;
yield 3;
} finally {
yield 4;
yield 5;
}
yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }
next()
、throw()
、return()
的共同点
next()
、throw()
、return()
这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让Generator函数恢复执行,并且使用不同的语句替换yield
表达式。
next()
是将yield
表达式替换成一个值。throw()
是将yield
表达式替换成一个throw
语句。return()
是将yield
表达式替换成一个return
语句。
yield*
表达式
如果在Generator函数内部,调用另一个Generator函数,默认情况下是没有效果的。
这个就需要用到yield*
表达式,用来在一个Generator函数里面执行另一个Generator函数。
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"
下面的例子,如果g4
前面没有使用yield*
,返回值就是遍历器对象,而不是遍历器的内部值
function * g3() {
yield 100;
yield 200;
yield g4();
yield 300;
}
function * g4() {
yield 1000;
yield 2000;
yield 300;
}
let result3 = g3();
yield*
后面的Generator函数(没有return
语句时),等同于在Generator函数内部,部署一个for...of
循环。
function* concat(iter1, iter2) {
yield* iter1;
yield* iter2;
}
// 等同于
function* concat(iter1, iter2) {
for (var value of iter1) {
yield value;
}
for (var value of iter2) {
yield value;
}
}
上面代码说明,yield*
后面的Generator函数(没有return
语句时),不过是for...of
的一种简写形式,完全可以用后者替代前者。反之,在有return
语句时,则需要用var value = yield* iterator
的形式获取return
语句的值。
如果yield*
后面跟着一个数组,由于数组原生支持遍历器,因此就会遍历数组成员。
function* gen(){
yield* ["a", "b", "c"];
}
gen().next() // { value:"a", done:false }
可以利用上面这一点来将多维数组扁平化:
let arr = [1, [2, [3, 4, [5]]]];
function * getTree(arr) {
if (Array.isArray(arr)) {
for (let i of arr) {
yield * getTree(i)
}
} else {
yield arr
}
}
console.log([...getTree(arr)]); // [1, 2, 3, 4, 5]
generator 函数作为对象属性
可以利用对象方法属性的简写方法,在前面加上*
即可
let obj = {
* myGeneratorMethod() {
···
}
};
状态机
可以用generator函数实现一个状态机,每执行一次,改变一次状态
// 状态机
let clock = function * () {
while (true) {
console.log(true);
yield;
console.log(false);
yield
}
}
这种状态机,没有用来保存状态的外部变量,更加简洁、安全,因为Generator本身就包含了状态信息
应用
异步操作的同步化表达
Generator函数的暂停执行的效果,意味着可以把异步操作写在yield
表达式里面,等到调用next
方法时再往后执行。
这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield
表达式下面,反正要等到调用next
方法时再执行。
所以,Generator函数的一个重要实际意义就是用来处理异步操作,改写回调函数。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 等待加载完成后卸载UI
loader.next()
也可以利用Generator部署Ajax操作:
function* main() {
var result = yield request("http://some.url");
var resp = JSON.parse(result);
console.log(resp.value);
}
function request(url) {
makeAjaxCall(url, function(response){
it.next(response);
});
}
var it = main();
it.next();