ES6必知必会 (七)—— Generator 函数

Generator 函数

1.Generator 函数是 ES6 提供的一种异步编程解决方案,语法行为与传统函数完全不同,通常有两个特征:

  • function关键字与函数名之间有一个星号;

  • 函数体内部使用yield表达式,定义不同的内部状态
    //一个简单的 Generator 函数

    function *Generator(){
         yield 'Hello';
         yield 'World';
    
        return 'Hello World';
    }
    

2.Generator 函数的调用方法与普通函数一样,也是在函数名后面加上一对圆括号。不同的是,调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象,必须调用 next 方法,才能使得指针移向下一个状态。也就是说,每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。实际上就是,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行。

function *Generator(){
    yield 'Hello';
    yield 'World';

    return 'Hello World';
}

let generator = Generator();  //无返回

generator.next();  
//{"value":"Hello","done":false}

generator.next();
//{"value":"World","done":false}

generator.next();
//{"value":"Hello World","done":true}

generator.next();
//{"value":undefined , "done":true}

上述代码就是一个 Generator 函数的执行过程 :

  • 第一次调用 next() 方法, 遇到第一个 yield 表达式后返回一个对象,对象的 vlaue 属性值是第一个 yield 表达式的值 Hello , done属性是 false , 表示整个遍历还没有结束;
  • 第二次调用 next() 方法, Generator 函数从上次yield表达式停下的地方继续执行 , 遇到第二个 yield 表达式后返回一个对象,对象的 vlaue 属性值是第一个 yield 表达式的值 World , done属性是 false , 表示整个遍历还没有结束;
  • 第三次调用 next() 方法, Generator 函数从上次yield表达式停下的地方继续执行 ,一直执行到return语句(如果没有return语句,就执行到函数结束)。此时 next 方法返回的对象的value属性,就是 return语句后面的表达式的值(如果没有return语句,则value属性的值为undefined),done属性的值true,表示遍历已经结束;
  • 第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefined,done属性为true。以后再调用next方法,返回的都是这个值。

3.上述例子中我们可以得知,调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着value和done两个属性的对象。value属性表示当前的内部状态的值,是yield表达式(或return)后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

4.yield 表达式可以看做是Generator 函数的暂停标志,要注意的是 yield 表达式后面的表达式,只有当调用next方法、内部指针指向该语句时才会执行;

function* gen() {
  yield  123 + 456;
}

上面代码中,yield后面的表达式123 + 456,不会立即求值,只会在next方法将指针移到这一句时,才会求值(“惰性求值”);

5.yield 表达式与 return 语句都能返回紧跟在语句后面的那个表达式的值,不同的是遇到yield,函数暂停执行,下一次再从该位置继续向后执行,而 return 语句不具备位置记忆的功能,而且一个函数里面,只能执行一个 return 语句,但是可以执行多个yield表达式,要注意的是 yield 表达式只能用在 Generator 函数里面,用在其他地方都会报错。

(function (){
  yield 1;
})()
// SyntaxError: Unexpected number

6.Generator 函数可以不用yield表达式,这时就变成了一个单纯的暂缓执行函数

function *Generator() {
  console.log('Hello World!')
}

var generator = Generator();

setTimeout(function () {
  generator.next()
}, 2000);

// "Hello World"

7.yield 表达式如果用在另一个表达式之中,必须放在圆括号里面,如果用作函数参数或放在赋值表达式的右边,可以不加括号

function *Generator() {
  console.log('Hello' + yield);         // SyntaxError
  console.log('Hello' + yield 123);     // SyntaxError

  console.log('Hello' + (yield));       // OK
  console.log('Hello' + (yield 123));   // OK
}

function *Generator() {
  foo( yield 'a' , yield 'b' );         // OK
  let input = yield;                    // OK
}

8.yield 表达式本身没有返回值,或者说是返回 undefined。next 方法可以带一个参数,该参数就会被当作上一个 yield 表达式的返回值。

function *Generator() {
  for(var i = 0; true; i++) {
    var reset = yield i;
    if( reset ) { i = -1; }
  }
}

var g = Generator();

g.next()        // { value: 0, done: false }
g.next()        // { value: 1, done: false }
g.next(true)    // { value: 0, done: false }

上述代码中,定义了一个可以无限运行的 Generator 函数,如果 next 方法没有参数,每次运行到 yield 表达式,变量 reset 的值总是 undefined ,当 next 方法带一个参数true时,变量reset就被重置为这个参数(即true),因此i会等于-1,下一轮循环就会从-1开始递增。

我们利用这个特性,在 Generator 函数运行的不同阶段,从外部向内部注入不同的值,从而调整函数行为;

function *foo(x) {
  var y = 2 * (yield (x + 1));
  var z = yield (y / 3);
  return (x + y + z);
}

var a = foo(5);
a.next()            // {value:6, done:false}
a.next()            // {value:NaN, done:false}
a.next()            // {value:NaN, done:true}

var b = foo(5);
b.next()            // { value:6, done:false }
b.next(12)          // { value:8, done:false }
b.next(13)          // { value:42, done:true }

上述代码 a 第二次运行 next 方法的时候不带参数,导致 y 的值等于2 * undefined(即NaN),除以3以后还是NaN,因此返回对象的value属性也等于NaN。第三次运行Next方法的时候不带参数,所以z等于undefined,返回对象的value属性等于5 + NaN + undefined,即NaN;

b 调用加了参数,结果就不一样了,第一次调用next方法时,返回 x+1 的值 6;第二次调用next方法,将上一次 yield 表达式的值设为 12 ,因此 y 等于 24,返回 y / 3 的值 8;第三次调用next方法,将上一次 yield 表达式的值设为 13 ,因此 z 等于 13 ,这时 x 等于 5,y 等于24,所以 return 语句的值等于 42;

ps:next 方法的参数表示上一个yield表达式的返回值,所以在第一次使用next方法时,传递参数是无效的

9.for...of循环可以自动遍历 Generator 函数时生成的Iterator对象,且此时不再需要调用next方法;

function *foo() {
  yield 1;
  yield 2;
  yield 3;
  yield 4;
  yield 5;
  return 6;
}

for ( let v of foo() ) {
  console.log(v);
}

// 1 2 3 4 5

上述代码,使用了 for ... of 循环 , 依次打印了5个 yield 表达式的值,此时就不需要 next 方法了,但是要注意的是,一旦 next 方法的返回对象的 done 属性为 true时,for...of循环就会中止,且不包含该返回对象,因此上面 return 语句返回的 6,不包括在for...of循环之中。

10.Generator函数返回的遍历器对象,还有一个return方法,可以返回给定的值,并且终结遍历Generator函数

function *Generator() {
  yield 1;
  yield 2;
  yield 3;
}

var g = Generator();

g.next()        // { value: 1, done: false }
g.return('ok')  // { value: "ok", done: true }
g.next()        // { value: undefined, done: true }

遍历器对象 g 调用 return 方法后,返回值的 value 属性就是 return 方法的参数 ok(如果 不提供参数,则返回值的 value 属性为 undefined) 。并且,Generator函数的遍历就终止了,返回值的 done 属性为 true,以后再调用 next 方法,value 总是返回 undefined , done 属性总是返回 true;

11.如果在 Generator 函数内部,调用另一个 Generator 函数,默认情况下是没有效果的。

function *Generator1() {
  yield 'a';
  yield 'b';
}

function *Generator2() {
  yield 'x';
  Generator1();
  yield 'y';
}

for (let v of Generator2()){
  console.log(v);
}
// "x"
// "y"

12.可以使用 yield* 表达式,用来在一个 Generator 函数里面执行另一个 Generator 函数来达到在 Generator 函数内部,调用另一个 Generator 函数的效果;

function *Generator1() {
  yield 'a';
  yield 'b';
}

function *Generator2() {
  yield 'x';
  yield* Generator1();
  yield 'y';
}

// 等同于
function *Generator2() {
  yield 'x';
  yield 'a';
  yield 'b';
  yield 'y';
}

// 等同于
function *Generator2() {
  yield 'x';
  for (let v of Generator2()) {
    yield v;
  }
    yield 'y';
}

for (let v of Generator2()){
  console.log(v);
}
// "x"
// "a"
// "b"
// "y"

13.Generator 可以暂停函数执行,返回任意表达式的值。这种特点使得 Generator 有多种应用场景;

  • 异步操作的同步化表达 , 可以利用 Generator 函数的暂停执行的效果,把异步操作写在 yield 表达式里面,等到调用next方法时再往后执行
    function *loading() {
    showLoadingScreen();
    yield loadDataAsync();
    hideLoadingScreen();
    }
    var loader = loading();
    // 加载loading
    loader.next()

     // 隐藏loading
     loader.next()
    

上面代码中,第一次调用 loading 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用next方法,则会显示Loading界面(showLoadingScreen),并且异步加载数据(loadDataAsync)。等到数据加载完成,再一次使用next方法,则会隐藏Loading界面,整个逻辑就很清晰了~

  • 可以利用 Generator 函数用同步的方式部署 Ajax 操作

     function *main(url) {
       var result = yield request( url );
       var resp = JSON.parse(result);
       console.log(resp.value);
     }
    
     function request(url) {
       makeAjaxCall(url, function(response){
      it.next(response);
       });
     }
    
     var ajax = main();
     ajax.next();
    

上面代码的 main 函数,就是通过 Ajax 操作获取数据,要注意的是 makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因为 yield 表达式,本身是没有值的,总是等于undefined;

  • 控制流管理,如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样:

     step1(function (value1) {
       step2(value1, function(value2) {
         step3(value2, function(value3) {
           step4(value3, function(value4) {
             // Do something with value4
           });
         });
       });
     });
    

如果采用 Promise 写法 ,

     Promise.resolve(step1)
      .then(step2)
      .then(step3)
      .then(step4)
      .then(function (value4) {
        // Do something with value4
      }, function (error) {
        // Handle any error from step1 through step4
      })

采用 Generator 函数来写 :

    function *longRunningTask(value1) {
      try {
      var value2 = yield step1(value1);
      var value3 = yield step2(value2);
      var value4 = yield step3(value3);
      var value5 = yield step4(value4);
      // Do something with value4
      } catch (e) {
      // Handle any error from step1 through step4
      }
    }

然后使用一个自动化函数,按次序执行所有步骤

    scheduler(longRunningTask(initialValue));

    function scheduler(task) {
      var taskObj = task.next(task.value);
      // 如果Generator函数未结束,就继续调用
      if (!taskObj.done) {
        task.value = taskObj.value
        scheduler(task);
      }
    }

14.另外,Generator 函数是协程在 ES6 的实现,最大特点就是可以交出函数的执行权(即暂停执行),遇到 yield 命令就暂停,等到执行权返回,再从暂停的地方继续往后执行,另外,它的函数体内外的数据交换和错误处理机制的特点使它可以作为异步编程的完整解决方案;

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 200,738评论 5 472
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,377评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 147,774评论 0 333
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,032评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,015评论 5 361
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,239评论 1 278
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,724评论 3 393
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,374评论 0 255
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,508评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,410评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,457评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,132评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,733评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,804评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,022评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,515评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,116评论 2 341

推荐阅读更多精彩内容