1.简介
Generator函数是ES6提供的一种异步编程解决方案。
语法上,可以把它理解成,Generator函数是一个状态机,封装了多个内部状态。
执行Generator函数会返回一个遍历器对象,也就是说,Generator函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历Generator函数内部的每一个状态。
Generator函数有两个特征:
(1)funtion关键字和函数名之间有个*号
(2)函数体内部使用yield表达式,定义不同的内部状态
下面这个生成器函数内部有两个yield表达式,因此该函数有3个状态:hello,world和return 语句。
执行Generator函数后,返回一个迭代器对象传给hw。
const myGenerator = function* () {
yield 'hello';
yield 'world';
return 'end';
}
const hw = myGenerator();
下一步,通过迭代器对象的next方法,移动指针至下一个状态。
hw.next(); // { value: 'hello', done: false }
hw.next(); // { value: 'world', done: false }
hw.next(); // { value: 'ending', done: true }
hw.next(); // { value: undefined, done: true }
也就是说,每次调用next方法,指针就从上一次停下来的地方执行,直到遇到下一个yield/return。
换言之,Generator函数是分段执行的,yield/return表达式是暂停执行的标记,next方法用于恢复执行。
2.yield和next
2.1 yield基本作用
首先yield用来暂停,next用来继续上文已提,这里带过。
yield后面的表达式只有调用next方法使得内部指针指向该语句时才会执行。
比如这样,console语句不会先执行,而是遍历到它了才打印3。
const myGenerator = function* () {
yield 'hello';
yield console.log(1+2);
return 'end';
}
如果Generator函数内没有yield表达式,就起到暂缓执行的作用。
例如这个例子,如果是普通函数的话,在为it赋值时就会执行;因为是Generator函数,所以只有在调用next方法时才会执行。
const myGenerator = function* () {
console.log('暂缓执行')
}
const it = myGenerator();
setTimeout(()=>{
it.next();
} , 1000)
2.2 与Iterator接口的关系
如果直接用扩展运算符遍历一个对象会报错,因为对象不具有Symbol.iterator属性,也就不具有Iterator接口。
const obj = { 'a': 1, 'b': 2 };
console.log(...obj)
//Uncaught TypeError: Spread syntax requires ...iterable[Symbol.iterator] to be a function
由于Generator就是生成遍历器的函数,因此可以把Generator赋值给对象的Symbol.iterator属性,使其具有Iterator接口。
const obj = { 'a': 1, 'b': 2 };
obj[Symbol.iterator] = function* () {
yield 1;
yield 2;
yield 3;
};
console.log(...obj) // 1 2 3
Generator函数执行后,返回一个遍历器对象。该对象本身也具有Symbol.iterator属性,执行后返回自身。
function* gen() {
// some code
}
var g = gen();
g[Symbol.iterator]() === g //true
2.3 next方法的参数
yield的返回值始终是undefined(注意例如yield 123并不是返回123 而是通过指针才能访问到value:123)。
next方法可以传入一个参数,该参数会被当作上一个yield表达式的返回值。
例如这个例子,如果next方法没有参数,那么每次运行到yield,reset的值始终是undefined。直到next方法中传入参数true,使得reset == true,使得i == -1,因此下一轮循环就会从-1开始递增。
const fn = function* () {
for (let i = 0; true; i++) {
const reset = yield i;
if (reset) i = -1;
}
}
const it = fn();
console.log(it.next()); //{value: 0, done: false}
console.log(it.next()); //{value: 1, done: false}
console.log(it.next()); //{value: 2, done: false}
console.log(it.next(true)); //{value: 0, done: false}
通过这个技巧,就有办法在Generator函数开始运行之后,继续向函数体内部注入值,从而调整函数行为。
const foo = function* (x) {
let y = 2 * (yield (x + 1));
let z = yield (y / 3);
return x + y + z;
}
//由于第二个it1.next()不带参数 导致y = 2*undefined = NaN
const it1 = foo(5);
it1.next() //{value:6, done:false}
it1.next() //{value:NaN, done:false}
it1.next() //{value:NaN, done:true}
const it2 = foo(5);
it2.next(); //{value:6, done:false}
//令yield(x+1)=12 则y=24 z = yield 8
it2.next(12); //{ value:8, done:false }
//令yield(y/3)=13 则z=13 x+y+z=5+24+13=42
it2.next(13); // { value:42, done:true }
或者是这样传参
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
3. for...of遍历Generator
因为Generator函数生成Iterator对象,所以可以通过for...of来遍历。
function* foo(){
yield 1;
yield 2;
yield 3;
return 4;
}
for(let v of foo()){
console.log(v); //1 2 3
}
除了for...of,扩展运算符,解构赋值,Array.from等方法内部调用的都是遍历器接口,意味着都可以将Generator返回的Iterator对象作为参数。
function* numbers() {
yield 1
yield 2
return 3
yield 4
}
// 扩展运算符
[...numbers()] // [1, 2]
// Array.from 方法
Array.from(numbers()) // [1, 2]
// 解构赋值
let [x, y] = numbers();
//x = 1
//y = 2
4. Generator.prototype.throw()
Generator返回的遍历器对象,都自带一个throw方法,可以在函数体外抛出错误,在Generator函数体内捕获。
相当于将yield替换成一个throw语句
const g = function* (x, y) {
let result = yield x + y;
return result;
};
gen.throw(new Error('出错了')); // Uncaught Error: 出错了
// 相当于将 let result = yield x + y
// 替换成 let result = throw(new Error('出错了'));
如这个例子,it连续throw两个错误,第一个被函数体内catch捕获;第二次throw由于内部的catch语句已经执行过了,错误就被抛出函数体,被外部的catch捕获。
const myGenerator = function*(){
try{
yield;
}catch(e){
console.log('内部捕获' , e);
}
}
const it = myGenerator();
it.next();
try{
it.throw('aaa');
it.throw('bbb');
}catch(e){
console.log('外部捕获' , e);
}
要注意的是throw方法抛出的错误要被内部捕获,前提是必须至少执行过一次next方法。如果把那句it.next()去掉函数内部就捕获不到了。
但是要注意如果是用全局的throw抛出,那么就只能被外部捕获。(当然catch也只能捕获一次)
try {
throw new Error('a');
throw new Error('b');
} catch (e) {
console.log('外部捕获', e);
}
throw方法被捕获以后,会附带执行下一条yield表达式。也就是说,会附带执行一次next方法。
const myGenerator = function* () {
try {
yield console.log('a');
} catch (e) {
console.log('内部捕获', e);
}
yield console.log('b');
}
const it = myGenerator();
it.next(); //a
it.throw('sorry');
//内部捕获 sorry
//b
一旦执行过程中错误抛出并没有被内部捕获,就不会执行下去了。如果此后还调用next方法,将返回一个value属性等于undefined、done属性等于true的对象。
function* g() {
yield 1;
console.log('throwing an exception');
throw new Error('generator broke!');
yield 2;
yield 3;
}
function log(generator) {
var v;
console.log('starting generator');
try {
v = generator.next();
console.log('第一次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第二次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
try {
v = generator.next();
console.log('第三次运行next方法', v);
} catch (err) {
console.log('捕捉错误', v);
}
console.log('caller done');
}
log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done
5. 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 }
如果Generator函数内部有try...finally代码块,且正在执行try代码块,那么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 }
其实就相当于将yield表达式替换成一个return语句
const g = function* (x, y) {
let result = yield x + y;
return result;
};
gen.return(2); // Object {value: 2, done: true}
// 相当于将 let result = yield x + y
// 替换成 let result = return 2;
6.yield*
当在一个Generator内部要调用另一个Generator时用到的表达式。
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';
}
同理,任何数据结构只要具有Iterator接口,就可以被yield*遍历
function* gen() {
yield* ["a", "b", "c"];
}
//遍历数组
gen().next() // { value:"a", done:false }
let read = (function* () {
yield 'hello';
yield* 'hello';
})();
//遍历字符串
read.next().value // "hello"
read.next().value // "h"
7.应用
7.1 用同步方式写异步
Generator 函数的暂停执行的效果,意味着可以把异步操作写在yield表达式里面,等到调用next方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在yield表达式下面,反正要等到调用next方法时再执行。
function* loadUI() {
showLoadingScreen();
yield loadUIDataAsynchronously();
hideLoadingScreen();
}
var loader = loadUI();
// 加载UI
loader.next()
// 卸载UI
loader.next()
逐行读取文本文件
function* numbers() {
let file = new FileReader("numbers.txt");
try {
while (!file.eof) {
yield parseInt(file.readLine(), 10);
}
} finally {
file.close();
}
7.2 控制流管理(任务均为同步操作)
回调地狱
step1(function (value1) {
step2(value1, function (value2) {
step3(value2, function (value3) {
step4(value3, function (value4) {
// Do something with value4
});
});
});
});
Promise
Promise.resolve(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
// Do something with value4
}, function (error) {
// Handle any error from step1 through step4
})
.done();
Generator
function* longRunningTask(value1) {
try {
var value2 = yield step1(value1);
var value3 = yield step2(value2);
var value4 = yield step3(value3);
var value5 = yield step4(value4);
// Do something with value4
} catch (e) {
// Handle any error from step1 through step4
}
}
7.3 作为数据结构
Generator由于返回遍历器可以看作是一个数组结构,可以对任意表达式提供类似数组的接口。
//返回三个函数
function* doStuff() {
yield fs.readFile.bind(null, 'hello.txt');
yield fs.readFile.bind(null, 'world.txt');
yield fs.readFile.bind(null, 'and-such.txt');
}
//可以像处理数组那样处理这三个函数
for (task of doStuff()) {
// task是一个函数,可以像回调函数那样使用它
}