生成器和Promise

一、生成器函数

类似于python,js也有自己的生成器,其基本的使用和定义方式如下所示:

1
2
3
4
5
6
7
8
9
function* myGenertor(){
yield 'test1';
yield 'test2';
yield 'test3';
}

for(let aa of myGenertor()){
console.log(aa)
}

最后,这个程序依次输出的是test1.test2和test3,这也是JavaScript的一个生成器。在定义函数的时候在function后面加一个*表示这是一个生成器函数,并通过关键字yield返回相应的值。并且,对于生成器函数来说,它不会像普通函数一样停止执行,而是当一个值的请求到来后,生成器就会从上次离开的地方继续开始执行。示例如下:

1
2
3
4
5
6
7
8
9
10
11
function* myGenertor(){
var bb = 1;
yield bb;
bb++;
yield bb;
yield bb;
}

for(let aa of myGenertor()){
console.log(aa)
}

从上面这个例子可知,第一次调用myGenenrtor这个生成器函数的时候,首先定义了一个名为bb的变量并且初始化为1,然后将这个值返回。第二次调用的时候,生成器不会像普通函数那样,再次从函数的开始执行,而是从上次执行的语句之后又开始执行b++,并返回此时bb的值为2.然后继续这个过程,返回的值是3.在这里,for-of循环就是对迭代器进行迭代的语法糖。

二、通过迭代对象控制生成器

首先看下面这个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function* myGenertor(){
var bb = 1;
yield bb;
bb++;
yield bb;
}

var myIterator = myGenertor();
console.log(myIterator);
console.log(myIterator.next())

console.log(myIterator.next())
console.log(myIterator.next())
//执行的结果为
//{}
//{ value: 1, done: false }
//{ value: 2, done: false }
//{ value: undefined, done: true }

在上面这个例子中,依然定义了一个生成器函数myGenertor,接下来调用这个生成器函数,但这一次赋给了一个名为myIterator的一个变量,如果直接输出这个变量,得到的是一个空的对象,只有对这个对象执行接口方法next(),生成器才开始执行代码,当代码执行到yield关键字的时候,生成一个中间结果并且返回一个新的对象,其中封装了一个结果值和指示完成的指示器。每当生成器生成一个值之后,生成器就会非阻塞挂起,等待下一次请求的到达。当没有可执行的代码的时候,生成器就会返回一个value为“undefined”,done为true的值,表示他的状态已完成。

三、利用生成器函数把执行权交给下一个生成器

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* myGenerator(action){
const imposter = yield("hello" + action);
//第二次next开始执行
console.log(imposter)
yield("nihao (" + imposter + ")" + action);
}

const test = myGenerator("test1");
console.log(test.next())
console.log(test.next("myotherTest"))

//执行结果:
// { value: 'hellotest1', done: false }
// myotherTest
// { value: 'nihao (myotherTest)test1', done: false }

​ 上面这个实例中,在迭代器上使用yield*操作符,程序会跳转到另一个生成器上执行,这一切对于最初调用的迭代器而言都是透明的。

四、迭代器与生成器之间的交互

现在来讨论以下迭代器与生成器的交互问题,从而更好的理解生成器与迭代器之间的关系。首先先来看下面这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* myGenerator(action){
const imposter = yield("hello" + action);
//第二次next开始执行
console.log(imposter)
yield("nihao (" + imposter + ")" + action);
}

const test = myGenerator("test1");
console.log(test.next())
console.log(test.next("myotherTest"))

//执行结果:
// { value: 'hellotest1', done: false }
// myotherTest
// { value: 'nihao (myotherTest)test1', done: false }

​ 这里,我们给生成器一个参数action。首先新建一个迭代器并赋给test这样的一个变量。并且这个时候,我们已经给myGenerator赋予了一个参数,所以在下一次调用的时候,会输出”hello”加上这个参数。之后,生成器就在yield这里被挂起,imposter始终都没有被赋值直到下一次再次调用next()的时候,此时imposter被赋予了next中的值,这里是因为yield的返回值就是下一次调用next()的参数,所以imposter就被赋值。这也是为什么不能通过第一次调用next()向生成器提供参数的原因,但还是可以在构造的时候提供初值。(构造的时候仅仅是给参数赋值,但生成器函数是不会运行的)

五、生成器异常和处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function* myGenerator(data){
try{
yield "hello";
}catch(e){
console.log("出错了!!" + e)
}
}
const test1 = myGenerator();
console.log(test1.next());
test1.throw("迭代器抛出了一个错误");


// 运行结果:
// { value: 'hello', done: false }
// 出错了!!迭代器抛出了一个错误

​ 这里将生成器中全部的代码装入try-catch块中去。在创建了迭代器之后,通过调用迭代器中的throw方法抛出异常,并利用生成器函数中的catch语句进行异常的接受和处理。

六、promise和生成器解决异步问题

首先测试下面的这段代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function delay(time){
var promise = new Promise(function(resolve,reject){
setTimeout(resolve,time)
})
return promise;
}
function* test(){
yield delay(300).then(() => console.log(1));
yield console.log(2)
yield delay(300).then(() => console.log(3));
yield console.log(4)
}
const a = test()
a.next();
a.next();
a.next();
a.next();

//输出为2,4,1,3

由于在生成器中存在异步操作,所以在使用next操作的时候,会存在异步问题。这时候检查第一个a.next()的返回值的时候,可以看到返回以下的结果:

1
2
{ value: Promise { <pending> }, done: false }
1

说明,对于一个promise,它的then()函数的返回值同样是一个promise对象。利用这个特性,将下次想要顺序执行的同步操作中的值放在promise函数的then之后,就可以得到我们想要的结果。

1
2
3
4
5
6
7
const a = test()
a.next().value.then(() => {
a.next()
a.next().value.then(() => {
a.next()
})
})

可以发现,如果在遇到异步请求的时候,就使用.then(),如果遇到同步操作,就直接next()即可,所以可将上述代码整理成为下面的方法,利用递归来实现这个过程:

1
2
3
4
5
6
7
8
9
10
11
12
13
function himmel(gen) {
const item = gen.next()
if (item.done) {
return ;
}

const { value, done } = item
if (value instanceof Promise) {
value.then((e) => himmel(gen))
} else {
himmel(gen)
}
}

最终,执行的结果就是按照顺序执行的1,2,3,4​

###番外

异步问题的另一种解决方案——利用闭包来实现:

首先还是先看下面这个异步的例子:

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
function delay(time){
var promise = new Promise(function(resolve,reject){
setTimeout(resolve,time)
})
return promise;
}

function test(){
for(var i = 0;i < 5;i++){
delay(300).then(function(){
console.log('这是第' + i + '一段输出');
})
console.log('这个应该在第' + i + '次异步操作执行结束后才输出')
if(i == 4){
console.log('end')
}
}
}

test()

//运行结果
//这个应该在第0次异步操作执行结束后才输出
//这个应该在第1次异步操作执行结束后才输出
//这个应该在第2次异步操作执行结束后才输出
//这个应该在第3次异步操作执行结束后才输出
//这个应该在第4次异步操作执行结束后才输出
//end
//这是第5一段输出
//这是第5一段输出
//这是第5一段输出
//这是第5一段输出
//这是第5一段输出

​ 上面的代码是一个典型的异步的问题,由于delay()是一个异步操作,所以在程序运行到delay的时候,将这个操作放在异步事件的队列中,先去进行同步操作,于是本该在异步操作执行结束之后执行的语句被优先执行了。并且程序在异步操作还没来得及开始就宣布结束了。并且之后,由于全局变量中的i已经增加到了5,所以最后得到的都是5,这显然不是我们预期的结果。首先解决i的问题,这里需要加一个闭包,将每次执行的i纪录下来,并且利用递归,在每次异步操作回调之后在递归下一次的操作,直到达到程序的终点为止。这样也就完成了程序从异步变为同步的操作。具体代码如下所示:

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
function delay(time){
var promise = new Promise(function(resolve,reject){
setTimeout(resolve,time)
})
return promise;
}

function test(){
(function iter(i){
delay(300).then(function(){
console.log('这是第' + i + '一段输出');
console.log('这个应该在第' + i + '次异步操作执行结束后才输出')
if(i == 4){
console.log('end')
}else{
iter(i+1)
}
})
})(0)
}

test()
//运行结果:
//这是第0一段输出
//这个应该在第0次异步操作执行结束后才输出
//这是第1一段输出
//这个应该在第1次异步操作执行结束后才输出
//这是第2一段输出
//这个应该在第2次异步操作执行结束后才输出
//这是第3一段输出
//这个应该在第3次异步操作执行结束后才输出
//这是第4一段输出
//这个应该在第4次异步操作执行结束后才输出
//end
0%