02月27, 2018

Generator 函数

Generator 简介

 Generator官方的解释是生成器,但是语法上,首先可以把它理解成,Generator 函数是一个状态机,封装了多个内部状态。  Generator函数首先也是一个函数,语法上在定义时比普通函数多了一个*号,以下方式都时合法的

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

其次它与普通函数不同的是,它在调用后并不会立即执行,而是返回一个迭代器对象,然后通过每次调用该迭代器对象的next()分步执行。知道结束或者遇到return

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

var hw = helloWorldGenerator();
console.log(hw)//{}
console.log(hw.next())//{ value: 'hello', done: false }
console.log(hw.next())//{ value: 'world', done: false }
console.log(hw.next())//{ value: 'world', done: false }
console.log(hw.next())//{ value: undefined, done: true }

从上叙代码可以看出,每次调用遍历器对象的next方之后该函数返回一个包装着yield中值的对象。下一次就会返回包装下一个yield后边的值的对象。这个对象有两个属性,一个value,代表着放回值,done代表中是否遍历完毕,当遍历完毕时value的值为undefined。   Generator函数作为对象的属性是可以和普通函数一样简写,但是需要在函数名前边加一个*号

let obj = {
  * myGeneratorMethod() {
    //code
  }
};
//等价于
let obj = {
  myGeneratorMethod: function* () {
    //code
  }
};

简单结束 yield

在上一个例子中,Generator函数体存在一个yield关键字,这个单词的字面意思是产出,在这里它代表了一种暂停的状态。我们发现只有调用next方法才会遍历下一个内部状态,所以其实提供了一种可以暂停执行的函数。   遍历器对象的next方法的运行逻辑如下。

  • (1) 遇到yield表达式,就暂停执行后面的操作,并将紧跟在yield后面的那个表达式的值,作为返回的对象的value属性值。
  • (2) 下一次调用next方法时,再继续往下执行,直到遇到下一个yield表达式。
  • (3) 如果没有再遇到新的yield表达式,就一直运行到函数结束,直到return语句为止,并将return语句后面的表达式的值,作为返回的对象的value属性值。
  • (4) 如果该函数没有return语句,则返回的对象的value属性值为undefined。

yield关键字的作用看上去和return非常类似,但是return并不具备记忆功能,而yield可以返回多次。yield后边可以是任意一个对象,这也意味着可以是一个自执行函数。一个Generator函数体中如果没有yield可以单纯理解成一个暂缓执行函数。 另外需要注意,yield表达式只能用在Generator函数里面,用在其他地方都会报错。

function* helloWorldGenerator() {
    yield function () {
        console.log("hello")
        return 'hello'
    }();

    yield console.log("world");

    return 'ending';
}

var hw = helloWorldGenerator();
let next = hw.next();
console.log(next)

//打印结果
//hello
//{ value: 'hello', done: false }

next 方法的参数

在上边的例子中,我们都把yield当作return来使,其实它的功能远不止如此,我们还可以通过next方法的参数,将值赋给yield,再供给其他代码使用。

function* foo() {
    let msg = yield;
    console.log(`${msg} word` )
}

var iterator = foo();
console.log(iterator.next())//{ value: undefined, done: false }
iterator.next('hello')//hello word

通过以上例子我们可以看到,iterator的第一次next时,代码执行到了yield关键字后返回了undefined,在第二次调用next我们给了它一个'hello'参数,神奇的事情发生了,let msg = yield;这句话把'hello'的值赋给了msg,利用这个特性我们可以做一下有趣的事情。

function* foo() {
    let flag = yield ;
    if(flag){
        let msg = yield;
        console.log(`${msg} word` )
    }else{
        console.log(`hahaha` )
    }


}

const flag = true;
var iterator = foo();
iterator.next();
iterator.next(flag) 
iterator.next('hello')

/**
    当flag = true 时 打印 hello word
    当flag = true 时 打印 hahaha
*/

从上边的例子中我们可以发现,我们可以通过改变第二步传入的参数来改变程序最终的执行结果。

for...of 循环

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
 */

 //遍历任何一个对象
function* objectEntries(obj) {
  let propKeys = Reflect.ownKeys(obj);

  for (let propKey of propKeys) {
    yield [propKey, obj[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

for (let [key, value] of objectEntries(jane)) {
  console.log(`${key}: ${value}`);
}


//将 Generator 函数加到对象的Symbol.iterator属性上面。
function* objectEntries() {
  let propKeys = Object.keys(this);

  for (let propKey of propKeys) {
    yield [propKey, this[propKey]];
  }
}

let jane = { first: 'Jane', last: 'Doe' };

jane[Symbol.iterator] = objectEntries;

for (let [key, value] of jane) {
  console.log(`${key}: ${value}`);
}

Generator.prototype 原型

Generator.prototype.next()

返回 yield 表达式产生的值. next()是将yield表达式替换成一个值。

Generator.prototype.throw()

向生成器抛出错误. throw()是将yield表达式替换成一个throw语句。如果 Generator 函数内部和外部,都没有部署try...catch代码块,那么程序将报错,直接中断执行。

var gen = function* gen(){
    try {
        yield console.log('a');
    } catch (e) {
        // ...
    }
    yield console.log('b');
    yield console.log('c');
}

var g = gen();
g.next() // a
g.throw() // b
g.next() // c

异常的捕捉

function* g() {
  yield 1;
  console.log('throwing an exception');
  throw new Error('generator broke!');
  yield 2;
  yield 3;
}

function log(generator) {
  var v;
  console.log('starting generator');
  try {
    v = generator.next();
    console.log('第一次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第二次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  try {
    v = generator.next();
    console.log('第三次运行next方法', v);
  } catch (err) {
    console.log('捕捉错误', v);
  }
  console.log('caller done');
}

log(g());
// starting generator
// 第一次运行next方法 { value: 1, done: false }
// throwing an exception
// 捕捉错误 { value: 1, done: false }
// 第三次运行next方法 { value: undefined, done: true }
// caller done

Generator.prototype.return()

返回给定的值并结束生成器。return()是将yield表达式替换成一个return语句。 如果 Generator 函数内部有try...finally代码块,那么return方法会推迟到finally代码块执行完再执行。

function* numbers () {
  yield 1;
  try {
    yield 2;
    yield 3;
  } finally {
    yield 4;
    yield 5;
  }
  yield 6;
}
var g = numbers();
g.next() // { value: 1, done: false }
g.next() // { value: 2, done: false }
g.return(7) // { value: 4, done: false }
g.next() // { value: 5, done: false }
g.next() // { value: 7, done: true }

yield* 表达式

如果需要在一个Generator函数中调用另外一个Generator函数时,就会用到yield*表达式,它的用法和 yield 一样。

function* bar() {
    yield 'a';
    yield* foo();//在*bar()中调用*foo()
    yield 'e';
}

function* foo() {
    yield 'b';
    yield 'c';
    yield 'd';
}

for (let v of bar()){
    console.log(v);
}
//a,b,c,d,e

如果不在yield后边加上*号会出现什么情况呢?

function* foo() {
    yield 'hello!';
    yield 'world!';
}

function* bar() {
    yield 'open';
    yield foo();
    yield 'close';
}

var gen = bar()
console.log(gen.next().value )// "open"
let iterator = gen.next().value;
console.log(iterator) //{} 返回一个遍历器对象
console.log(gen.next().value )// "close"
console.log(gen.next().value )// "undefined" 到这一步已经没有了
console.log("===============")
console.log(iterator.next().value)//hello!
console.log(iterator.next().value)//world!

从上两个简单的例子中可以看到,yield*会使其后边的Generator函数嵌套到当前的Generator函数中,而不加*号时,只是普通的调用的该Generator函数返回它的迭代器。

  从语法角度看,如果yield表达式后面跟的是一个遍历器对象,需要在yield表达式后面加上星号,表明它返回的是一个遍历器对象。这被称为yield表达式。yield后面的 Generator 函数(没有return语句时),等同于在 Generator 函数内部,部署一个for...of循环。实际上,任何数据结构只要有 Iterator 接口,就可以被yield*遍历。

function* foo(){
    yield* ["a", "b", "c"];
}

for (let v of foo()){
    console.log(v);
}
// a,b,c

let read = (function* () {
  yield* 'hello';
})();

for(let v of read){
    console.log(v)
}
//h,e,l,l,o

Generator 函数的this

  Generator 函数总是返回一个遍历器,ES6 规定这个遍历器是 Generator 函数的实例,也继承了 Generator, 函数的prototype对象上的方法。生成器函数不能当构造器使用

    function* foo(){
    yield* ["a", "b", "c"];
}

let foo1 = new foo();//foo is not a constructor


function* F() {
  yield this.x = 2;
  yield this.y = 3;
}

new F() // TypeError: F is not a constructor

当在Generator 函数前边加上new时,运行回报** is not a constructor的错误。

function* g() {
  this.a = 11;
}

let obj = g();
obj.next();
obj.a // undefined

我们在Generator 函数g在this对象上面添加了一个属性a,但是obj对象拿不到这个属性。

Generator 与协程

  协程(coroutine)是一种程序运行的方式,可以理解成“协作的线程”或“协作的函数”。协程既可以用单线程实现,也可以用多线程实现。前者是一种特殊的子例程,后者是一种特殊的线程。

协程与子例程的差异

  传统的“子例程”(subroutine)采用堆栈式“后进先出”的执行方式,只有当调用的子函数完全执行完毕,才会结束执行父函数。协程与其不同,多个线程(单线程情况下,即多个函数)可以并行执行,但是只有一个线程(或函数)处于正在运行的状态,其他线程(或函数)都处于暂停态(suspended),线程(或函数)之间可以交换执行权。也就是说,一个线程(或函数)执行到一半,可以暂停执行,将执行权交给另一个线程(或函数),等到稍后收回执行权的时候,再恢复执行。这种可以并行执行、交换执行权的线程(或函数),就称为协程。

  从实现上看,在内存中,子例程只使用一个栈(stack),而协程是同时存在多个栈,但只有一个栈是在运行状态,也就是说,协程是以多占用内存为代价,实现多任务的并行。   

协程与普通线程的差异

 不难看出,协程适合用于多任务运行的环境。在这个意义上,它与普通的线程很相似,都有自己的执行上下文、可以分享全局变量。它们的不同之处在于,同一时间可以有多个线程处于运行状态,但是运行的协程只能有一个,其他协程都处于暂停状态。此外,普通的线程是抢先式的,到底哪个线程优先得到资源,必须由运行环境决定,但是协程是合作式的,执行权由协程自己分配。

  由于 JavaScript 是单线程语言,只能保持一个调用栈。引入协程以后,每个任务可以保持自己的调用栈。这样做的最大好处,就是抛出错误的时候,可以找到原始的调用栈。不至于像异步操作的回调函数那样,一旦出错,原始的调用栈早就结束。

  Generator 函数是 ES6 对协程的实现,但属于不完全实现。Generator 函数被称为“半协程”(semi-coroutine),意思是只有 Generator 函数的调用者,才能将程序的执行权还给 Generator 函数。如果是完全执行的协程,任何函数都可以让暂停的协程继续执行。

  如果将 Generator 函数当作协程,完全可以将多个需要互相协作的任务写成 Generator 函数,它们之间使用yield表示式交换控制权。   

Generator 与上下文

  JavaScript 代码运行时,会产生一个全局的上下文环境(context,又称运行环境),包含了当前所有的变量和对象。然后,执行函数(或块级代码)的时候,又会在当前上下文环境的上层,产生一个函数运行的上下文,变成当前(active)的上下文,由此形成一个上下文环境的堆栈(context stack)。

  这个堆栈是“后进先出”的数据结构,最后产生的上下文环境首先执行完成,退出堆栈,然后再执行完成它下层的上下文,直至所有代码执行完成,堆栈清空。

  Generator 函数不是这样,它执行产生的上下文环境,一旦遇到yield命令,就会暂时退出堆栈,但是并不消失,里面的所有变量和对象会冻结在当前状态。等到对它执行next命令时,这个上下文环境又会重新加入调用栈,冻结的变量和对象恢复执行。   

应用

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

只能使异步操作在逻辑上同步化表示出来

function* main() {
    var result = yield doSomethingAsync();
    var resp = result.value;
    resp.then(function (value) {
        console.log(`异步操作返回结果 ${value}`)
        console.log("结束")
    })
}

function doSomethingAsync() {
    return Promise.resolve().then(function () {
        for (let i = 0; i < 1000000; i++) {
        }
        console.log("异步操作处理完毕")
        return "hello";
    })
}


var it = main();
console.log("开始")
//开始异步操作,并获取异步结果
let value = it.next();
//...进行其他操作处理
console.log("进行其他操作处理")
//处理异步的结果
it.next(value);

/**
    开始
    进行其他操作处理
    异步操作处理完毕
    异步操作返回结果 hello
    结束
 */

参考

阮一峰ECMAScript 6 入门

官方文档

本文链接:https://www.qiangshuidiyu.xin/post/generator.html

-- EOF --

Comments