生成器

大家对于异步这个词想必都不陌生,看到异步我可能最先想到的就是使用回调,再者我会使用Promise,可是使用回调处理异步控制流是有缺陷的:第一,基于回调的异步方式实在不适合大脑对于任务的思考(这点我深有体会);第二,存在控制反转的问题。而Promise链提供了我们以顺序的方式处理和表达异步流。而这篇文章的主角生成器(generator)也是一种顺序和看似同步的处理异步的方式,并且它比Promise更加优秀。

有关函数

不知道大家有没有这样思考过,一个函数一旦执行,其是否能够中断,我也没有仔细想这个问题,并且当看到这个问题,我会经验判断函数在执行过程中不会中断。到底是否中断,我们先写一个例子:

1
2
3
4
5
6
7
8
9
10
var a = 1;
function foo(){
a++;
bar();
console.log(a);
}
function bar(){
a++;
}
foo(); // 3

运行上面这个例子,最终的结果是3,这就意味着,当函数执行完a++后,函数foo从表面上来看被中断了,然后执行了bar函数,最后执行权交给foo,函数返回已经被修改的变量a的值。虽然通过上面的小例子,并且因为JS单线程的特性,我们似乎能够肯定JS是抢占式的,但实际情况是JS并不是抢占式的,虽然函数bar的执行打断了函数foo的执行,但这其实是一种“关联”(参考原型继承)。

初识生成器

现在让我们来认识生成器表达式,
同样是上面的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
var a = 1;
function *foo(){
a++;
yield;
console.log(a);
}
function bar(){
a++;
}
var iter = foo();
iter.next();
bar();
iter.next(); // 3

分析一下:
首先我们创建了一个生成器foo(注意*),该函数里面多了一个yield,有过python经验的大概知道这是干什么的。然后创建了一个函数bar;再来看执行,这里var iter = foo()并没有执行生成器foo,而是构建了一个迭代器,然后使用next方法启动了迭代器foo,并且在碰到yield停止执行,此时已经执行了x++,然后执行函数bar,执行完后,x经过两轮自增,此时x的值为3;最后我们调用next方法,从上一次中断处继续执行,并且没有碰到yield表达式,一直执行到函数结束,打印变量x的值为3。

现在我们来回答什么是生成器,其是一个特殊的函数,在函数声明的时候函数名前面包含一个”*“,并且能够多次启动和暂停。好了,我认为仅此而已。

一些问题

为了把生成器运用到异步流程控制中,我们还需要更深层次的了解生成器。

生成器仍然是一种函数

既然说生成器其本质上还是一种函数,所以其仍然具备函数最基本的特性,能够传递参数,也能返回值。我们不妨写一个例子测试一下:

1
2
3
4
5
6
7
8
9
function *test(a, b){
a++;
yield;
return a + b;
}
var it = test(1,2);
it.next();
var res = it.next();
console.log(res.value); // 4

其实生成器和普通函数的一个很大区别是在运行期间,它不会直接运行,而是创建了一个迭代器,然后每调用迭代器的next方法,便向下执行直到到碰到yield或者执行完成暂停。而我们注意到调用next方法返回的其实是一个对象,其包含一个value属性,如果生成器返回值的话,那么该属性的值就为生成器返回的值。总的来说,生成器的运行到目前为止完全是依托迭代器。

好了,继续。

继续来看例子:

1
2
3
4
5
6
7
8
9
10
function *foo(a, b){
a++;
yield a;
return a + b;
}
var it = foo(1,2);
var mid = it.next();
console.log(mid.value); // 2
var res = it.next();
console.log(res.next); // 4

这个例子想表达的就是对于生成器而言,yield表达式总会返回一个值,而我们可以通过迭代器next方法生成的对象来得到。

继续来看一个更复杂的例子:

1
2
3
4
5
6
7
8
9
function *foo(a){
var b = a + (yield 'ok return!');
return b;
}
var it = foo(1);
var res = it.next();
console.log(res.value); // ok return!
res = it.next(2);
console.log(res.value); // 3

这个例子向我们展示了生成器是如何双向传递消息的,yield表达式可以发出消息响应next(…)的调用,而next(…)可以向暂停的yield表达式发送值。

多个迭代器

一个生成器的多个实例可以并发执行,并且可以彼此交互。

1
2
3
4
5
6
7
8
9
10
function *gen(a){
var b = a + (yield)
return b
}
var it1 = gen(1);
var it2 = gen(2);
it1.next();
var res = it1.next(3).value;
it2.next();
console.log(it2.next(res).value); // 6

这个例子想说明的是一个生成器的多个实例可以并发执行,并且是互不干扰的。

分享到 评论