迭代器(Iterator)和生成器(Generator)之二

迭代器的基础功能可以辅助我们完成很多任务,通过生成器创建迭代器的过程也很便捷,迭代器也可以被用于完成一些复杂任务。

给迭代器传递参数

给迭代器next()方法传递参数,则这个参数的值会替代生成器内部上一条yield语句的返回值。

1
2
3
4
5
6
7
8
9
10
function *createInterator(){
let first = yield 1;
let second = yield first + 2; //4+2
yield second + 3; //5+3
}
let iterator = createInterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next(4)); //{ value: 6, done: false }
console.log(iterator.next(5)); //{ value: 8, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

第一次调用next()方法,无论传入什么参数都会被丢弃。因为第一次调用next()方法前不会执行任何yield语句。

第二次调用next()方法传入数值4做为参数,它最后被赋值给生成器函数内部的变量first。在一个含参yield语句中,表达式右侧等价于第一次调用next()方法后的下一个返回值,表达式左侧等价于第二次调用next()方法后,在函数继续执行前得到的返回值。第二次调用next()方法传入的值为4,他会被赋值给变量first,函数则继续执行。

代码执行过程为

1
2
3
1. next()---> yield 1
2. next(4)---> let first = 4; yield first + 2;
3. next(5)---> let second = 5; yield second + 3;

在迭代器中抛出错误

通过throw()方法,当迭代器恢复进行时可令其抛出一个错误。这种主动抛出错误的能力对于异步编程而言至关重要,从而增强生成器内部的编程弹性。

1
2
3
4
5
6
7
8
9
function *createInterator(){
let first = yield 1;
let second = yield first + 2; //yield 4 + 2然后抛出错误
yield second + 3; //不会执行
}
let iterator = createInterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next(4)); //{ value: 6, done: false }
console.log(iterator.throw(new Error('boom')));

调用throw()方法后,在继续执行let second求值前,错误就会被抛出并阻止代码继续执行。可以使用try...catch代码块来捕获这些错误。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function *createInterator(){
let first = yield 1;
let second;
try {
second = yield first + 2;
} catch (error) {
second = 6;
}
yield second + 3;
}
let iterator = createInterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next(4)); //{ value: 6, done: false }
console.log(iterator.throw(new Error('boom'))); //{ value: 9, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

在变量second赋值前会主动抛出错误,catch代码块捕获到错误后,将second变量赋值为6,下一条yield语句继续执行后。

调用throw()方法后也会像调用next()方法一样返回一个结果对象,由于在生成器内部捕获了这个错误,因而会继续执行下一条yield语句。next()方法使迭代器继续执行,throw()方法也会使迭代器继续执行,并同时抛出一个错误,在此之后的执行过程取决于生成器内部的代码。

生成器返回语句

由于生成器也是函数,因此可以通过return语句提前退出函数执行。在生成器中,return表示所有操作已经完成,属性done被设置为true,如果同时提供了相应的值,则属性value会被设置成这个值。

1
2
3
4
5
6
7
8
9
function *createInterator(){
yield 1;
return 2;
yield 3;
}
let iterator = createInterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next()); //{ value: 2, done: true }
console.log(iterator.next()); //{ value: undefined, done: true }

return语句后面的yield语句不会被执行。通过return语句指定的返回值,只会在返回对象中出现一次,后续调用返回对象中,value属性会被重置为undefined

展开运算符与for-of循环语句会直接忽略通过return语句指定的任何返回值,只要done一变为true就立即停止读取其他的值。

委托生成器

某些情况下,需要将两个迭代器合二为一,这时可以创建一个生成器,再给yield语句添加一个星号,就可以将生成数据的过程委托给其他生成器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *createNumberIterator(){
yield 1;
yield 2;
}
function *createColorInterator(){
yield 'red';
yield 'green';
}
function *createCombinedInterator(){
yield *createNumberIterator();
yield *createColorInterator();
yield true;
}
var iterator = createCombinedInterator();
console.log(iterator.next()); //{ value: 1, done: false }
console.log(iterator.next()); //{ value: 2, done: false }
console.log(iterator.next()); //{ value: 'red', done: false }
console.log(iterator.next()); //{ value: 'green', done: false }
console.log(iterator.next()); //{ value: true, done: false }
console.log(iterator.next()); //{ value: undefined, done: true }

生成器createCombinedInterator()先后委托了另外两个生成器createNumberIterator()createColorInterator()。每一次调用next()方法就会委托相应的迭代器生成相应的值,知道两个迭代器无法返回更多的值,此时执行最后一条yield语句并返回true

yield *也可以用于字符串,此时将使用字符串的默认迭代器。

异步任务

异步处理一般方式是使用回调函数,由于生成器支持在函数中暂停代码执行,所以有更优雅的处理方式。

执行yield语句会暂停当前函数的执行过程并等待下一次调用next()方法,可以创建一个函数,在函数中调用生成器生成相应的迭代器,从而不用回调函数的基础上实现异步调用next()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function run(taskDef) {
//创建迭代器
let task = taskDef();
//开始执行任务
let result = task.next();
//循环调用next()的函数
function step(){
//任务没完成,则继续执行
if(!result.done){
result = task.next();
step();
}
}
step();
}
run(function *(){
console.log(1);
yield;
console.log(2);
yield;
console.log(3);
})

函数run接受一个生成器函数作为参数,这个函数定义了后续要执行的任务。首次调用迭代器的next()方法,返回的结果被存起来后续使用,step函数会检查result.done的值,来确定是否还有任务需要执行。

给任务执行器传递数据最简单的办法是,将值通过迭代器的next()方法传入做为yield的生成器供下次使用。

1
2
3
4
5
6
function step(){
if(!result.done){
result = task.next(result.value));
step();
}
}

上面是在多个yield调用间来回传递静态数据,而等待一个异步过程有点不一样。可以给yield一个函数,这个函数返回一个可以执行回调函数的函数。需要修改一下任务执行函数,当result.value是一个函数时,执行这个函数,在将结果传给next()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
function run(taskDef) {
let task = taskDef();
let result = task.next();
function step(){
if(!result.done){
if(typeof result.value === 'function'){
result.value(function(err, data){
if(err){
result = task.throw(err);
return ;
}
result = task.next(data);
step();
})
}else{
result = task.next(result.value);
step();
}
}
}
step();
}
var fetchData = ()=>{
return cb => {
setTimeout(()=>{
cb(null, 'hello world')
}, 1000)
}
}
run(function *(){
console.log(1);
let data = yield fetchData();
console.log(data);
yield;
console.log(3);
})
//1,(1s后)'hello world',3

这里回调函数遵循了Node.js中执行错误的约定,错误放在第一个参数中。