yield和yield*的区别和用法

    先前在学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
    2
    3
    4
    5
    6
    7
    function* foo(){
    var index = 0;
    while (index <= 2) // when index reaches 3,
    // yield's done will be true
    // and its value will be undefined;
    yield index++;
    }
    1
    2
    3
    4
    5
    var iterator = foo();
    console.log(iterator.next()); // { value:0, done:false }
    console.log(iterator.next()); // { value:1, done:false }
    console.log(iterator.next()); // { value:2, done:false }
    console.log(iterator.next()); // { value:undefined, done:true }

    上面的都是我从MDN上学习到的,我没有做任何改动,因为我觉得它本身的例子就很好。看到这里,你肯定会想,那么为什么yield会跟异步扯上关系呢?因为执行到yield的时候,本次调用就已经结束了。控制权已经转到了外部next方法。并且调用的过程中整个生成器内部状态是一直在改变的。如果外部不条用next的话,那么这个生成器就停在了yield那里。所以我们只需要把异步的东西先做完。然后再在合适的地方调用next方法继续执行该生成器。就可以了。这就像函数在暂停,后面在继续的感觉。也就是我们通常理解的代码分段执行了。在阮一峰老师的es6的书里,他有提到,所谓的异步,就可以理解为代码分段执行了。先执行了一部分,然后这部分没有执行完,就开始执行下一部分。等到第一部分执行完再来执行剩余的部分。这样就可以理解为简单的异步。后面的代码并没有等前面的代码执行完,就开始执行了。

    但实际上,我们会经常这么用:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    function 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
    18
    function * 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
    28
    var 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
    20
    function* 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
    15
    function* 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
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    function* g4() {
    yield* [1, 2, 3];
    return "foo";
    }
    var result;
    function* g5() {
    result = yield* g4();
    }
    var iterator = g5();
    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: undefined, done: true },
    // 此时 g4() 返回了 { value: "foo", done: true }
    console.log(result); // "foo"

    如果不用yield *?

    好,那如果我们想试一下不用yield*,还是用yield会得到什么结果呢?我们把上面的代码改成这样:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    function* 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
    6
    Object { 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里面是很棒的代码。决定这个星期再仔细学一遍。然后整理下,嘻嘻。


    观点

    这里有几个观点和技巧,是我犯过的错误。总结下:

    1. yield后面只能接generator?错误,比如你看到yield fun(),那么这个fun()的返回一定是generator吗?当然不是;fun方法完全可以返回一个 Promise,返回一个 thunk,返回一个数组、对象,或者就是返回generator objectyield后面可以接的值,要多注意容易犯错。

    2. 比如说你看到了yield * fun(),yield * 后面这个fun的返回值一定是generator object?yield*后面可以接很多,但是由于我们这里给的前提条件是yield*所以是可以判定的。
      这个是肯定的啦。你可以很自信的告诉别人这就是generator object

    3. 生成器其实在其它语言很早就有了,比如python、c#,但与python不同的是js的generator更多的是提供一种异步解决方案。yield也是,在python中都有。

    4. yield只能在koa里用?当然不是。koa里面只是利用yield,generator这种方式。yield,generator的用处可大了多了。


    总结

    主要就是学习了yield 和yield *的区别和联系,还有他们的使用方式。参考了下面的几篇文章,感谢:

    1. MDN-Operators/yield
    2. MDN-Operators/yield*
    3. Understanding-the-Yield-principle