先前在学koa的时候,学习到了很多概念,比如generator,yield,yield *再深一点,比如co,trunk,iterator,async,await这些。当时学习的时候还是有很多疑惑。现在又重新整理了一遍。感觉思路清晰了很多。记录分享如下。大部分都来自MDN的整理和学习。感谢。我觉得MDN那种先定义,再解释,再讲用途的方式特别好。如果再加上自己感性点的理解就是很完美的学习新东西的方式。以后要多学习这种学东西的习惯。很多时候,都是因为开始的时候没有抓住定义,把握住这个东西到底是干什么用的,导致到后来越来越糊涂。正确的做事情,第一次虽然会花费很长时间,但是后来会越来越少。
什么是yield?
yield的定义
yield 关键字用来暂停和继续一个生成器函数 (function* or legacy generator).
1 | yield [[expression]]; |
yield 关键字使生成器函数暂停执行,并返回跟在它后面的表达式的当前值. 可以把它想成是 return 关键字的一个基于生成器的版本.
yield 关键字实际返回一个对象,包含两个属性, value 和 done. value 属性为 yield expression 的值, done 是一个布尔值用来指示生成器函数是否已经全部完成.
一旦在 yield expression 处暂停, 除非外部调用生成器的 next() 方法,否则生成器的代码将不能继续执行. 这使得可以对生成器的执行以及渐进式的返回值进行直接控制.
上面你能够理解,是建立在稍微知道一些generator
的基础上的。generator
我们称之为生成器,当你看到一个function *(){//...}
这种有*
的函数的时候,你就可以把这个函数称作generator
,在这个函数里面,你可以使用yield
。
煮个栗子
1 | function* foo(){ |
1 | var iterator = foo(); |
上面的都是我从MDN上学习到的,我没有做任何改动,因为我觉得它本身的例子就很好。看到这里,你肯定会想,那么为什么yield会跟异步扯上关系呢?因为执行到yield的时候,本次调用就已经结束了。控制权已经转到了外部next方法。并且调用的过程中整个生成器内部状态是一直在改变的。如果外部不条用next的话,那么这个生成器就停在了yield那里。所以我们只需要把异步的东西先做完。然后再在合适的地方调用next方法继续执行该生成器。就可以了。这就像函数在暂停,后面在继续的感觉。也就是我们通常理解的代码分段执行了。在阮一峰老师的es6的书里,他有提到,所谓的异步,就可以理解为代码分段执行了。先执行了一部分,然后这部分没有执行完,就开始执行下一部分。等到第一部分执行完再来执行剩余的部分。这样就可以理解为简单的异步。后面的代码并没有等前面的代码执行完,就开始执行了。
但实际上,我们会经常这么用:1
2
3
4
5
6
7
8
9function fetchResult(){
return new Promise((resolve,reject)=>{
// ...
})
}
function gen*(){
var result = yield fetchResult();
console.log(result);
}
fetchResult是一个异步的操作。比如返回的是一个Promise,那么如果你不用yield的时候,log出来的result是null,因为fetchResult是异步的。这个时候用yield就很需要了。
我们可以用yield来写一个斐波那契函数:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18function * feb(num){
var count = 0;
var current= 1;
var last = 0;
while(count++ < num){
yield current;
var temp = current;
current += last;
last = temp;
}
}
var f = feb(7),nxt;
var arr = [];
while(!(nxt = f.next()).done){
arr.push(nxt.value);
}
注意这里last和count都是从0开始的。最后的到结果:Array [ 1, 1, 2, 3, 5, 8, 13 ]
。因为这里的yield的作用就跟我们递归是很像很像的。
那么在koa里面又是怎么用yield的呢?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
28var koa = require('koa');
var app = koa();
// x-response-time
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
this.set('X-Response-Time', ms + 'ms');
});
// logger
app.use(function *(next){
var start = new Date;
yield next;
var ms = new Date - start;
console.log('%s %s - %s', this.method, this.url, ms);
});
// response
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
在app.use里面,只接受有*的generator,在这个里面,你可以调用yield next继续往下一直进行,等下面没有yield可以返回的时候,再从下往上执行。具体koa是怎么实现中间件的,可以翻翻博客里另外一篇文章。
什么时候用yield *
yield *的概念
在生成器中,yield* 可以把需要 yield 的值委托给另外一个生成器或者其他任意的可迭代对象。1
yield* [[expression]];
yield* 一个可迭代对象,就相当于把这个可迭代对象的所有迭代值分次 yield 出去。
yield* 表达式本身的值就是当前可迭代对象迭代完毕时的那个返回值(也就是迭代器的迭代值的 done 属性为 true 时 value 属性的值)。
可以在定义看到,yield是把值委托给*一个生成器或者是一个可以迭代的对象。下面举几个例子来说:
委托给其他生成器
以下代码中,g1() yield 出去的每个值都会在 g2() 的 next() 方法中返回,就像那些 yield 语句是写在 g2() 里一样。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20function* g1() {
yield 2;
yield 3;
yield 4;
}
function* g2() {
yield 1;
yield* g1();
yield 5;
}
var iterator = g2();
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: 3, done: false }
console.log(iterator.next()); // { value: 4, done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
委托给其他类型的可迭代对象
除了生成器对象这一种可迭代对象,yield* 还可以 yield 其它任意的可迭代对象,比如说数组、字符串、arguments 对象等等。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15function* g3() {
yield* [1, 2];
yield* "34";
yield* arguments;
}
var iterator = g3(5, 6);
console.log(iterator.next()); // { value: 1, done: false }
console.log(iterator.next()); // { value: 2, done: false }
console.log(iterator.next()); // { value: "3", done: false }
console.log(iterator.next()); // { value: "4", done: false }
console.log(iterator.next()); // { value: 5, done: false }
console.log(iterator.next()); // { value: 6, done: false }
console.log(iterator.next()); // { value: undefined, done: true }
yield* 表达式的值
1 | function* g4() { |
如果不用yield *?
好,那如果我们想试一下不用yield*,还是用yield会得到什么结果呢?我们把上面的代码改成这样:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21function* g1() {
yield 2;
yield 3;
yield 4;
}
//去掉*后,看看结果
function* g2() {
yield 1;
yield g1();
yield 5;
}
var iterator = g2();
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
console.log(iterator.next());
我们会得到如下的结果:1
2
3
4
5
6Object { value: 1, done: false }
Object { value: Generator, done: false }
Object { value: 5, done: false }
Object { value: undefined, done: true }
Object { value: undefined, done: true }
Object { value: undefined, done: true }
为什么会有这个结果呢?这就是yield *
的魔力了。yield *
后面可以接受一个iterable object
,然后这个yield* a
的值,就是这个a
完成时,也就是状态done:true
时的a的返回值。当你调用generator function
时,会返回一个generator object
,这个对象也是一个iterable object
。【yield*表达式本身的值就是当前可迭代对象迭代完毕时的那个返回值】。
其实最常用的就是yield*用来在一个 generator 函数里“执行”另一个 generator 函数,并可以取得其返回值。
这里还可以扯一些关于co的事情。你会发现在co里面,你是可以直接yield 一个generator的,更可怕的是还可以yield一个generator function的。这里面来源自co在实现的时候进行了判断。1
if (isGeneratorFunction(obj) || isGenerator(obj)) return co.call(this, obj);
当它判断出来你是一个generator function的时候,是会继续的调用自己的。在co里,如果是yield *fn差不多就等于yield co(fn)
co里面是很棒的代码。决定这个星期再仔细学一遍。然后整理下,嘻嘻。
观点
这里有几个观点和技巧,是我犯过的错误。总结下:
yield后面只能接generator?错误,比如你看到
yield fun()
,那么这个fun()的返回一定是generator吗?当然不是;fun方法完全可以返回一个 Promise,返回一个 thunk,返回一个数组、对象,或者就是返回generator object
。yield
后面可以接的值,要多注意容易犯错。比如说你看到了
yield * fun()
,yield *
后面这个fun的返回值一定是generator object
?yield*
后面可以接很多,但是由于我们这里给的前提条件是yield*
所以是可以判定的。
这个是肯定的啦。你可以很自信的告诉别人这就是generator object
。生成器其实在其它语言很早就有了,比如python、c#,但与python不同的是js的generator更多的是提供一种异步解决方案。yield也是,在python中都有。
yield只能在koa里用?当然不是。koa里面只是利用yield,generator这种方式。yield,generator的用处可大了多了。
总结
主要就是学习了yield 和yield *的区别和联系,还有他们的使用方式。参考了下面的几篇文章,感谢: