《ES6标准入门》(八)之generator函数

附上一个讲的还不错的ES6入门讲解连接:来自极客学院

Generator 函数

简介

所谓 Generator,有多种理解角度。首先,可以把它理解成一个函数的内部状态的遍历器,每调用一次,函数的内部状态发生一次改变(可以理解成发生某些事件)。ES6 引入 Generator 函数,作用就是可以完全控制函数的内部状态的变化,依次遍历这些状态。

在形式上,Generator 是一个普通函数,但是有两个特征。一是,function 命令与函数名之间有一个星号;二是,函数体内部使用 yield 语句,定义遍历器的每个成员,即不同的内部状态(yield 语句在英语里的意思就是“产出”)。

function* helloWorldGenerator() {
  yield 'hello';
  yield 'world';
  return 'ending';
}

var hw = helloWorldGenerator();

上面代码定义了一个 Generator 函数 helloWorldGenerator,它的遍历器有两个成员“hello”和“world”。调用这个函数,就会得到遍历器。

当调用 Generator 函数的时候,该函数并不执行,而是返回一个遍历器(可以理解成暂停执行)。以后,每次调用这个遍历器的 next 方法,就从函数体的头部或者上一次停下来的地方开始执行(可以理解成恢复执行),直到遇到下一个 yield 语句为止。也就是说,next 方法就是在遍历 yield 语句定义的内部状态。


hw.next()
// { value: 'hello', done: false }

hw.next()
// { value: 'world', done: false }

hw.next()
// { value: 'ending', done: true }

hw.next()
// { value: undefined, done: true }

上面代码一共调用了四次 next 方法。

第一次调用,函数开始执行,直到遇到第一句 yield 语句为止。next 方法返回一个对象,它的 value 属性就是当前 yield 语句的值 hello,done 属性的值 false,表示遍历还没有结束。

第二次调用,函数从上次 yield 语句停下的地方,一直执行到下一个 yield 语句。next 方法返回的对象的 value 属性就是当前 yield 语句的值 world,done 属性的值 false,表示遍历还没有结束。

第三次调用,函数从上次 yield 语句停下的地方,一直执行到 return 语句(如果没有 return 语句,就执行到函数结束)。next 方法返回的对象的 value 属性,就是紧跟在 return 语句后面的表达式的值(如果没有 return 语句,则 value 属性的值为 undefined),done 属性的值 true,表示遍历已经结束。

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

总结一下,Generator 函数使用 iterator 接口,每次调用 next 方法的返回值,就是一个标准的 iterator 返回值:有着 value 和 done 两个属性的对象。其中,value 是 yield 语句后面那个表达式的值,done 是一个布尔值,表示是否遍历结束。

上一章说过,任意一个对象的 Symbol.iterator 属性,等于该对象的遍历器函数,即调用该函数会返回该对象的一个遍历器。遍历器本身也是一个对象,它的 Symbol.iterator 属性执行后,返回自身。


function* gen(){
  // some code
}

var g = gen();

g[Symbol.iterator]() === g
// true

上面代码中,gen 是一个 Generator 函数,调用它会生成一个遍历器 g。遍历器 g 的 Symbol.iterator 属性是一个遍历器函数,执行后返回它自己。

由于 Generator 函数返回的遍历器,只有调用 next 方法才会遍历下一个成员,所以其实提供了一种可以暂停执行的函数。yield 语句就是暂停标志,next 方法遇到 yield,就会暂停执行后面的操作,并将紧跟在 yield 后面的那个表达式的值,作为返回对象的 value 属性的值。当下一次调用next方法时,再继续往下执行,直到遇到下一个 yield 语句。如果没有再遇到新的 yield 语句,就一直运行到函数结束,将 return 语句后面的表达式的值,作为 value 属性的值,如果该函数没有 return 语句,则 value 属性的值为 undefined。另一方面,由于 yield 后面的表达式,直到调用 next 方法时才会执行,因此等于为 JavaScript 提供了手动的“惰性求值”(Lazy Evaluation)的语法功能。

yield 语句与 return 语句有点像,都能返回紧跟在语句后面的那个表达式的值。区别在于每次遇到 yield,函数暂停执行,下一次再从该位置继续向后执行,而return语句不具备位置记忆的功能。一个函数里面,只能执行一次(或者说一个)return 语句,但是可以执行多次(或者说多个)yield 语句。正常函数只能返回一个值,因为只能执行一次 return;Generator 函数可以返回一系列的值,因为可以有任意多个 yield。从另一个角度看,也可以说 Generator 生成了一系列的值,这也就是它的名称的来历(在英语中,generator 这个词是“生成器”的意思)。

Generator 函数可以不用 yield 语句,这时就变成了一个单纯的暂缓执行函数。


function* f() {
  console.log('执行了!')
}

var generator = f();

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

上面代码中,函数f如果是普通函数,在为变量 generator 赋值时就会执行。但是,函数f是一个 Generator 函数,就变成只有调用 next 方法时,函数 f 才会执行。

另外需要注意,yield 语句不能用在普通函数中,否则会报错。


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

上面代码在一个普通函数中使用 yield 语句,结果产生一个句法错误。

下面是另外一个例子。


var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a){
  a.forEach(function(item){
    if (typeof item !== 'number'){
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)){
  console.log(f);
}

上面代码也会产生句法错误,因为 forEach 方法的参数是一个普通函数,但是在里面使用了 yield 语句。一种修改方法是改用 for 循环。

var arr = [1, [[2, 3], 4], [5, 6]];

var flat = function* (a){
  var length = a.length;
  for(var i =0;i<length;i++){
    var item = a[i];
    if (typeof item !== 'number'){
      yield* flat(item);
    } else {
      yield item;
    }
  }
};

for (var f of flat(arr)){
  console.log(f);
}
// 1, 2, 3, 4, 5, 6

next 方法的参数

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

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

var g = f();

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

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

这个功能有很重要的语法意义。Generator 函数从暂停状态到恢复运行,它的上下文状态(context)是不变的。通过 next 方法的参数,就有办法在 Generator 函数开始运行之后,继续向函数体内部注入值。也就是说,可以在 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() // Object{value:6, done:false}
a.next() // Object{value:NaN, done:false}
a.next() // Object{value:NaN, done:false}

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

如果向 next 方法提供参数,返回结果就完全不一样了。

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

var it = foo(5);

it.next()
// { value:6, done:false }
it.next(12)
// { value:8, done:false }
it.next(13)
// { value:42, done:true }

上面代码第一次调用 next 方法时,返回x+1的值 6;第二次调用 next 方法,将上一次 yield 语句的值设为 12,因此 y 等于 24,返回y / 3的值 8;第三次调用 next 方法,将上一次 yield 语句的值设为 13,因此 z 等于 13,这时 x 等于 5,y 等于 24,所以 return 语句的值等于 42。

注意,由于 next 方法的参数表示上一个 yield 语句的返回值,所以第一次使用 next 方法时,不能带有参数。V8 引擎直接忽略第一次使用 next 方法时的参数,只有从第二次使用 next 方法开始,参数才是有效的。

for...of 循环

for...of 循环可以自动遍历 Generator 函数,且此时不再需要调用 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 方法的返回对象的done属性为 true,for...of 循环就会中止,且不包含该返回对象,所以上面代码的 return 语句返回的 6,不包括在 for...of 循环之中。

下面是一个利用 generator 函数和 for...of 循环,实现斐波那契数列的例子。


function* fibonacci() {
  let [prev, curr] = [0, 1];
  for (;;) {
    [prev, curr] = [curr, prev + curr];
    yield curr;
  }
}

for (let n of fibonacci()) {
  if (n > 1000) break;
  console.log(n);
}

从上面代码可见,使用 for...of 语句时不需要使用 next 方法。

应用

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

(1)异步操作的同步化表达

Generator 函数的暂停执行的效果,意味着可以把异步操作写在 yield 语句里面,等到调用 next 方法时再往后执行。这实际上等同于不需要写回调函数了,因为异步操作的后续操作可以放在 yield 语句下面,反正要等到调用next 方法时再执行。所以,Generator 函数的一个重要实际意义就是用来处理异步操作,改写回调函数。


function* loadUI() { 
    showLoadingScreen(); 
    yield loadUIDataAsynchronously(); 
    hideLoadingScreen(); 
} 
var loader = loadUI();
// 加载UI
loader.next() 

// 卸载UI
loader.next()

上面代码表示,第一次调用 loadUI 函数时,该函数不会执行,仅返回一个遍历器。下一次对该遍历器调用 next 方法,则会显示 Loading 界面,并且异步加载数据。等到数据加载完成,再一次使用 next 方法,则会隐藏Loading 界面。可以看到,这种写法的好处是所有 Loading 界面的逻辑,都被封装在一个函数,按部就班非常清晰。

Ajax 是典型的异步操作,通过 Generator 函数部署 Ajax 操作,可以用同步的方式表达。


function* main() {
  var result = yield request("http://some.url");
  var resp = JSON.parse(result);
    console.log(resp.value);
}

function request(url) {
  makeAjaxCall(url, function(response){
    it.next(response);
  });
}

var it = main();
it.next();

上面代码的 main 函数,就是通过 Ajax 操作获取数据。可以看到,除了多了一个 yield,它几乎与同步操作的写法完全一样。注意,makeAjaxCall 函数中的 next 方法,必须加上 response 参数,因为 yield 语句构成的表达式,本身是没有值的,总是等于 undefined。

下面是另一个例子,通过 Generator 函数逐行读取文本文件。


function* numbers() {
    let file = new FileReader("numbers.txt");
    try {
        while(!file.eof) {
            yield parseInt(file.readLine(), 10);
        }
    } finally {
        file.close();
    }
}

上面代码打开文本文件,使用 yield 语句可以手动逐行读取文件。

(2)控制流管理

如果有一个多步操作非常耗时,采用回调函数,可能会写成下面这样。


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

采用 Promise 改写上面的代码。


Q.fcall(step1)
.then(step2)
.then(step3)
.then(step4)
.then(function (value4) {
    // Do something with value4
}, function (error) {
    // Handle any error from step1 through step4
})
.done();

上面代码已经把回调函数,改成了直线执行的形式,但是加入了大量 Promise 的语法。Generator 函数可以进一步改善代码运行流程。


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

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


scheduler(longRunningTask());

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

注意,yield 语句是同步运行,不是异步运行(否则就失去了取代回调函数的设计目的了)。实际操作中,一般让 yield 语句返回 Promise 对象。


var Q = require('q');

function delay(milliseconds) {
  var deferred = Q.defer();
  setTimeout(deferred.resolve, milliseconds);
  return deferred.promise;
}

function* f(){
  yield delay(100);
};

上面代码使用 Promise 的函数库 Q,yield 语句返回的就是一个 Promise 对象。

多个任务按顺序一个接一个执行时,yield 语句可以按顺序排列。多个任务需要并列执行时(比如只有 A 任务和 B 任务都执行完,才能执行 C 任务),可以采用数组的写法。


function* parallelDownloads() {
  let [text1,text2] = yield [
    taskA(),
    taskB()
  ];
  console.log(text1, text2);
}

上面代码中,yield 语句的参数是一个数组,成员就是两个任务 taskA 和 taskB,只有等这两个任务都完成了,才会接着执行下面的语句。

(3)部署 iterator 接口

利用 Generator 函数,可以在任意对象上部署 iterator 接口。


function* iterEntries(obj) {
    let keys = Object.keys(obj);
    for (let i=0; i < keys.length; i++) {
        let key = keys[i];
        yield [key, obj[key]];
    }
}

let myObj = { foo: 3, bar: 7 };

for (let [key, value] of iterEntries(myObj)) {
    console.log(key, value);
}

// foo 3
// bar 7

上述代码中,myObj 是一个普通对象,通过 iterEntries 函数,就有了 iterator 接口。也就是说,可以在任意对象上部署 next 方法。

下面是一个对数组部署 Iterator 接口的例子,尽管数组原生具有这个接口。


function* makeSimpleGenerator(array){
  var nextIndex = 0;

  while(nextIndex < array.length){
    yield array[nextIndex++];
  }
}

var gen = makeSimpleGenerator(['yo', 'ya']);

gen.next().value // 'yo'
gen.next().value // 'ya'
gen.next().done  // true

(4)作为数据结构

Generator 可以看作是数据结构,更确切地说,可以看作是一个数组结构,因为 Generator 函数可以返回一系列的值,这意味着它可以对任意表达式,提供类似数组的接口。


function *doStuff() {
  yield fs.readFile.bind(null, 'hello.txt');
  yield fs.readFile.bind(null, 'world.txt');
  yield fs.readFile.bind(null, 'and-such.txt');
}

上面代码就是依次返回三个函数,但是由于使用了 Generator 函数,导致可以像处理数组那样,处理这三个返回的函数。


for (task of doStuff()) {
  // task是一个函数,可以像回调函数那样使用它
}

实际上,如果用 ES5 表达,完全可以用数组模拟 Generator 的这种用法。


function doStuff() {
  return [
    fs.readFile.bind(null, 'hello.txt'),
    fs.readFile.bind(null, 'world.txt'),
    fs.readFile.bind(null, 'and-such.txt')
  ];
}

上面的函数,可以用一模一样的 for...of 循环处理!两相一比较,就不难看出 Generator 使得数据或者操作,具备了类似数组的接口。

猜你喜欢

转载自blog.csdn.net/weixin_37719279/article/details/81507084