ES6异步方案——生成器Generators/yield

一、Generator函数简介

  generator(生成器)是ES6标准引入的新的数据类型。一个generator看上去像一个函数,但可以返回多次。

  ES6定义generator标准时借鉴了Python的generator的概念和语法。

1、理解Generator函数

  Generator函数有多种理解角度。

function* gen() { 
  yield 1;
  yield 2;
  yield 3;
  return 'ending';
}

let g = gen(); 
// "Generator { }"

  上面代码定义了一个Generator函数 gen,它内部有三个yield 表达式,即该函数有四个状态:1,2,3和return语句(结束执行)。

(1)语法

  首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。执行 Generator 函数会返回一个遍历器对象

  也就是说,Generator 函数除了状态机,还是一个遍历器对象生成函数。返回的遍历器对象,可以依次遍历 Generator 函数内部的每一个状态。

(2)形式

  Generator 函数是一个普通函数,但是有两个特征。

  一是,function关键字与函数名之间有一个星号

  二是,函数体内部使用yield表达式,定义不同的内部状态(yield在英语里的意思就是“产出”)。

 2、next方法

  Generator.prototype.next()方法用于恢复执行,返回值是包含value和done属性的对象。

  调用 Generator 函数后,该函数并不执行,返回的也不是函数运行结果,而是一个指向内部状态的指针对象。

  因此必须调用遍历器对象的next方法,使得指针移动向下一个状态。

  每次调用next方法,内部指针就从函数头部或上一次停下来的地方开始执行,直到遇到下一个yield表达式(或return语句)为止。

  换言之,Generator 函数是分段执行的,yield表达式是暂停执行的标记,而next方法可以恢复执行

function* gen() {
    yield 1;
    yield 2;
    yield 3;
    return 'ending';
}

let g = gen();
// "Generator { }"

console.log(g.next());    // { value: 1, done: false }
console.log(g.next());    // { value: 2, done: false }
console.log(g.next());    // { value: 3, done: false }
console.log(g.next());    // { value: 'ending', done: true }

  上面的代码中调用了四次next方法。第一次调用,Gererator函数开始执行,直到遇到第一个yield表达式为止。next方法返回一个对象,value属性是当前yield表达式的值,done属性值为false,表示遍历未结束。

  第二次、第三次调用则是从上次yield表达式停止的地方开始,一直执行到下一个yield表达式。

  第四次调用,此时 Generator 函数已经运行完毕,next方法返回对象的value属性为undefineddone属性为true。以后再调用next方法,返回的都是这个值。

3、return方法

  Generator.prototype.return()方法用于立即结束遍历,并返回给定的值。

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

var g = gen();
console.log(g.next());         // { value: 1, done: false }
console.log(g.return('foo'));  // { value: "foo", done: true }
console.log(g.next());         // { value: undefined, done: true }

  Generator函数的返回值是遍历器对象,但可以用return方法指定返回的值。参数就是返回值的value属性。使用return方法后,done属性将设为true,立即终结遍历Generator函数。

  注意:
  1)如果遍历尚未结束,即done为false的情况下,调用无参的return(),会将value设为undefined。
  2)如果遍历已经结束,即done已经为true的情况下,调用return(value)是没有意义的,参数也不会生效,value会固定为undefined。

4、Generator调用

  调用 Generator 函数,返回一个遍历器对象,代表 Generator 函数的内部指针。以后,每次调用遍历器对象的next方法,就会返回一个有着valuedone两个属性的对象。value属性表示当前的内部状态的值,是yield表达式后面那个表达式的值;done属性是一个布尔值,表示是否遍历结束。

  ES6 没有规定,function关键字与函数名之间的星号,写在哪个位置。这导致下面的写法都能通过。

function * foo(x, y) { ··· }
function *foo(x, y) { ··· }
function* foo(x, y) { ··· }
function*foo(x, y) { ··· }

  由于 Generator 函数仍然是普通函数,所以一般的写法是上面的第三种,即星号紧跟在function关键字后面。

5、yield表达式

  Generator 函数返回的遍历器对象,只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。yield表达式就是暂停标志。

(1)next 方法运行逻辑

  1)遇到 yield 表达式,则暂停执行后面的操作,并紧跟在 yield 后面的那个表达式的值,作为返回的对象的 value 属性值。

  2)下次调用 next 方法时,再继续往下执行,直到遇到下一个 yield 表达式。

  3)若没有再遇到新的 yield 表达式,则一直运行到函数结束,直到 return 语句为止,并将 return 语句后面的表达式的值,作为返回的对象的 value 属性值。

  4)若该函数没有 return 语句,则返回的对象的 value 属性值为 undefined

(2)yield 与 return 对比

  yield表达式与return语句既有相似之处,也有区别。

  相似处:都能返回紧跟在语句后面的那个表达式的值。

  不同处:

  • 每次遇到yield,函数暂停执行,下一次再从该位置继续向后执行;return 语句不具备位置记忆的功能
  • 一个函数里面,可以执行多次(或者说多个)yield表达式;只能执行一次(或者说一个)return语句
  • Generator 函数可以返回一系列的值,因为可以有任意多个yield正常函数只能返回一个值,因为只能执行一次return

二、Generator函数异步应用

  由于Generator函数可以交出函数的执行权,整个Generator函数可以看作一个封装的异步任务,或者说是异步任务的容器

  异步操作需要暂停的地方,都用 yield 语句注明。Generator 函数的执行方法如下:

function* gen(x) {
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next() // { value: undefined, done: true }

  调用函数,返回一个内部指针(遍历器)g

1、Generator数据交换和错误处理

  Generator 函数可以暂停执行恢复执行,这是它能封装异步任务的根本原因。除此之外,它还有两个特性,使它可以作为异步编程的完整解决方案:函数体内外的数据交换错误处理机制

(1)next实现函数体内外数据交换

  next返回值的 value 属性,是 Generator 函数向外输出数据;next方法还可以接受参数,向 Generator 函数体内输入数据。

function* gen(x){
  var y = yield x + 2;
  return y;
}

var g = gen(1);
g.next() // { value: 3, done: false }
g.next(2) // { value: 2, done: true }

  第一个next方法的value属性,返回表达式x + 2的值3

  第二个next方法带有参数2,这个参数可以传入 Generator 函数,作为上个阶段异步任务的返回结果,被函数体内的变量y接收。因此,这一步的value属性,返回的就是2(变量y的值)。

(2)函数内部错误处理

  Generator 函数内部还可以部署错误处理代码,捕获函数体外抛出的错误。

function* gen(x){
  try {
    var y = yield x + 2;
  } catch (e){
    console.log(e);
  }
  return y;
}

var g = gen(1);
g.next();
g.throw('出错了');

  Generator 函数体外,使用指针对象的throw方法抛出的错误,可以被函数体内的try...catch代码块捕获。

  这意味着,出错的代码与处理错误的代码,实现了时间和空间上的分离,这对于异步编程无疑是很重要的。

2、异步任务封装

var fetch = require('node-fetch');

function* gen(){
  var url = 'https://api.github.com/users/github';
  var result = yield fetch(url);
  console.log(result.bio);
}

  上面代码中,Generator 函数封装了一个异步操作,该操作先读取一个远程接口,然后从 JSON 格式的数据解析信息。

  这段代码非常像同步操作,除了加上了yield命令。

  执行这段代码:

var g = gen();
var result = g.next();

result.value.then(function(data){
  return data.json();
}).then(function(data){
  g.next(data);
});

  首先执行 Generator 函数,获取遍历器对象,然后使用next方法(第二行),执行异步任务的第一阶段。由于Fetch模块返回的是一个 Promise 对象,因此要用then方法调用下一个next方法。

  由此可见,虽然Generator函数将异步操作表达很简洁,但流程管理不便。

 

posted @ 2020-04-28 15:05  休耕  阅读(593)  评论(0编辑  收藏  举报