yaofly2012 / note

Personal blog
https://github.com/yaofly2012/note/issues
44 stars 5 forks source link

Generator函数 #14

Open yaofly2012 opened 6 years ago

yaofly2012 commented 6 years ago

一、Generator function(生成器函数)

1.1 语法

function* name([param[, param[, ... param]]]) {
   statements
}
  1. 格式上跟普通函数就多个了星号*; 星号*function,函数名之间可以没有空格,一般把星号和function放一起。

  2. 生成器函数返回值是个生成器对象

    
    vvar toString = Object.prototype.toString;
    var gen = function* () {
    console.log('begin')
    yield 1;
    yield 2;
    return 5;
    }

var genObj = gen(); console.log(typeof gen) // function console.log(toString.call(gen)) // [object GeneratorFunction] console.log(gen instanceof Function) // true console.log(toString.call(genObj)) // [object Generator] console.log(typeof genObj) // object


3. 函数表达式,匿名函数都可以定义生成器函数;
4. 生成器函数可以作为对象和类的成员方法,但是**不可以作为**:
- 构造函数
构造函数有自己的返回值逻辑,并且构造函数是解决继承写法问题。虽然不能作为构造函数,但生成器函数也是有`prototype`属性的。
- 箭头函数
没有`function`关键字。

## 1.2 特性
1. 生成器函数可以终止,可以继续执行(反人类);
再次执行时上下文跟上次暂停时一样。
2. 生成器函数是“惰性的”,当调用生成器函数的时候不会立马执行,而是等继续(`next/throw/return`)执行时才执行相关代码,遇到下一个`yield`表达式时暂停(或者`return`/`throw`语句结束);
```js
var gen = function* () {
    console.log('begin')
    yield 1;
    yield 2;
    return 3;
}

var g = gen();
console.log('Generator created')
// [object Generator]
console.log(Object.prototype.toString.call(g))

console.log(g.next()) // 
console.log(g.next())

image

  1. return语句表示生成器函数最终的值
    
    var gen = function* () {
    yield 1;
    yield 2;
    return 3;
    }

var g = gen();

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


4. 如果生成器函数发生异常,则`next`方法直接向外抛:
```js
var gen = function* () {
    yield 1;
    throw new Error('error occured')
    yield 2;
    return 5;
}

var g = gen();

console.log(g.next())
try {
    console.log(g.next())
} catch(e) {
    console.error(e)
}
console.log(g.next())
console.log(g.next())

image

  1. yield*写法表示遍历其他生成器对象
    
    function* anotherGenerator(i) {
    yield i + 1;
    yield i + 2;
    yield i + 3;
    }

function generator(i) { yield i; yield anotherGenerator(i); yield i + 10; }

var gen = generator(10);

console.log(gen.next().value); // 10 console.log(gen.next().value); // 11 console.log(gen.next().value); // 12 console.log(gen.next().value); // 13 console.log(gen.next().value); // 20

`yield*`相当于:
```js
function* generator(i) {
  yield i;
  let ag = anotherGenerator(i);
  for(let val of ag) {
    yield val ;
  }
  yield i + 10;
}

本质上yield*后面可以是任意可迭代对象

var gen = function* () {
    yield 1;
    yield* [12, 123];
    return 3;
}

var g = gen();

for(let val of g) {
    console.log(val)
}

二、生成器对象(Generator)

生成器对象是一个内部含有状态的对象,通过并且只能通过生成器函数创建。

2.1 语法

生成器函数的返回值。

  1. 利用Object.prototype.toString判断生成器函数和生成器对象类型
    
    function* gen(i) {
    yield i.count + 1;
    yield i.count  + 2;
    yield i.count  + 3;
    }

var a = { count: 1 }; var g = gen(a)

Object.prototype.toString.call(gen) // [object GeneratorFunction] Object.prototype.toString.call(g) // [object Generator]

// 可迭代对象 var arr = [...g] console.log(arr) // [2, 3, 4] console.log(Symbol.iterator in g) // true

1. 跟null,undefined类型类似,并没有定义全局对象`GeneratorFunction`,`Generator`;
2. 生成器函数内部的`yield`表达式和`return`语句用于定义生成器对象的内部状态;
3. 生成器对象既是可迭代对象也是迭代器(其next方法符合迭代器协议)。

## 2.2 遍历
生成器对象本身就是个可迭代对象,但是只能一次性遍历。
```js
function* gen() {
    yield 1;
    yield 3;
    yield 5;
}
var g = gen();

// 第一次遍历
for(let num of g) {
    console.log(num);
}

// 第二次遍历- 
for(let num of g) {
    console.log(num); // 不会再执行了
}

2.3 APIs

1. next(value)

var a = foo(5); a.next() // Object{value:6, done:false},yield表达式的值为undefined a.next() // Object{value:NaN, done:false},yield表达式的值为undefined a.next() // Object{value:NaN, done:true},yield表达式的值为undefined

var b = foo(5); b.next() // { value:6, done:false },yield表达式的值为undefined b.next(12) // { value:8, done:false },yield表达式的值为12 b.next(13) // { value:42, done:true },yield表达式的值为13


- `next`方法的实参作为`yield`表达式的值;
外部可以用作向生成器对象传递值,并告诉生成器对象继续执行。

- `next`方法的返回值就是`yield`后面的表达式的值。
外部可以用作获取生成器对象内部的值

这种机制很重要,是实现异步同步化的关键。

### 2. [return(value)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Generator/return)
让生成器函数在上次暂停的地方(或者函数开始处)立马结束,并返回指定的状态值(return方法实参):
```js
{
  done: true,
  value: value // return方法的实参
}
  1. return方法可以多次调用,返回值的value属性是调用时的实参; 这个跟next方法的返回值逻辑不一样。
  2. 调用return方法时,生成器函数从上一次暂定的地方或者函数开始处就结束(即后面的代码不会再执行);
    
    var gen = function* () {
    console.log('begin run')
    throw new Error(1);;
    yield 1;
    yield 2;
    return 5;
    }

var g = gen(); // 立马结束,生成器函数没有执行代码的机会 console.log(g.return(1)) // {value: 1, done: true}


3. 立马结束和finally语句块必须执行的平衡
`try-finally`语句块中`finally`语句块中语句必须要执行,这跟`return`方法立马结束有点冲突。
当调用`return`方法时,如果上个`yield`表达式在`try`语句块中则`return`方法会推迟到`finally`代码块执行完再执行,并且可以修改返回值。即保证了`finally`的语法规则不变(一定要执行,可以修改try/catch中的返回值)。
```js
function* foo() {
    yield 1;
    try {
        yield 4;
    } finally {
        yield 5
    }
}

var a = foo();
console.log(a.next()) // {value: 1, done: false}
console.log(a.next()) // {value: 4, done: false}
console.log(a.return(2)) // {value: 5, done: false} ,继续执行finally语句块(新的状态,修改原return的值)
console.log(a.next()) // {value: 2, done: true} , return方法指定的终态值 
console.log(a.next()) // {value: undefined, true}

3. throw(exception)

让生成器函数在上次暂停的地方(或者函数开始处)抛出指定异常(throw方法实参)。

  1. throw返回值的取值逻辑同next方法; 可以把throw方法视为抛出异常的next方法。
  2. 生成器函数在什么节点抛异常?
    
    function* gen() {
    yield 1;
    yield 2;
    try {
    yield 3;
    } catch(e) {
    console.log('error catch in gen')
    }
    }

var gg = gen(); console.log(gg.next()); console.log(gg.next()); console.log(gg.next()); console.log(gg.throw('error occured'));

`throw`方法相当于让生成器函数在上次暂停的地方(或者函数开始处)处抛一个异常,如果生成器函数没有捕获,则会向外抛,则相当于调用`throw`处抛一个异常,也说明了函数开始处抛出的异常无法内部捕获。
```js
function* gen() {
  yield 1;
  yield 2;
  try {
    yield 3;
  } catch(e) {
    console.log('error catch in gen')
  }
}

var gg = gen();
console.log(gg.next());
console.log(gg.next());
console.log(gg.throw('error occured')); // 这个异常生成器函数没有捕获到

如果要捕获生成器没有捕获的异常,得在调用throw处捕获:

function* gen() {
  yield 1;
  yield 2;
  try {
    yield 3;
  } catch(e) {
    console.log('error catch in gen')
  }
}

var gg = gen();
console.log(gg.next());
console.log(gg.next());
try {
  console.log(gg.throw('error occured'));
} catch(e) {
   console.log('error catch in callee')
}
  1. 如果生成器函数如果没有捕获抛出的异常,则生成器函数会被终止。

4. 总结

  1. 生成器函数是创建生成器对象的工厂方法,可以参数化创建;
  2. 生成器对象的next/return/throw方法都是用于恢复生成器函数执行,区别是:
    • next:继续执行;
    • return:终止执行,并返回指定的值;
    • throw:终止执行,并抛出指定的异常。

这三个方法是外部控制生成器对象的执行,可用于实现异步逻辑同步化关键。

  1. 阮老师这个总结的到位 next()、throw()、return() 的共同点

    next()、throw()、return()这三个方法本质上是同一件事,可以放在一起理解。它们的作用都是让 Generator 函数恢复执行,并且使用不同的语句替换yield表达式。

用“语句替换”描述只是便于理解吧,但是有点误导。起码returnthrow方法可以在函数开始处立马结束(生成器函数都没有执行)

var gen = function* () {
    console.log('begin run')   
    yield 2;
    return 5;
}

var g = gen();
console.log(g.return(1)) //  或者console.log(g.throw(1))都是的生成器函数没有执行

三、生成器原理

先看看下面输出是什么:

// Demo1
var a = 0
function* gen(x) {
    a = a + (yield x);
    console.log(3, a)
}

var g = gen(10)
var r = g.next();
console.log(1, r.value)
a++;
console.log(2, a)
g.next(5);

稍微调整代码,下面输出是什么:

// Demo2
var a = 0
function* gen(x) {
    var b = yield x;
    a += b;
    console.log(3, a)
}

var g = gen(10)
var r = g.next();
console.log(1, r.value)
a++;
console.log(2, a)
g.next(5);

再稍微调整代码,下面输出是什么:

// Demo3
var a = 0
function* gen(x) {
    a =  (yield x) + a;
    console.log(3, a)
}

var g = gen(10)
var r = g.next();
console.log(1, r.value)
a++;
console.log(2, a)
g.next(5);
  1. 执行下文会被暂存 遇到yield表达式时,生成器函数暂停执行,退出调用栈,但是执行下文会被暂存。

  2. 恢复执行 暂存的执行上下文也被恢复。

回到上面的问题Demo1和Demo2的结果为啥不一样? 在Demo1中语句a = a + (yield x);算术运算从左向右执行的,等执行到yield x时变量a已经参与运算了,暂停执行的时候,会存在临时变量里了。相当于:

function* gen(x) {
    var tem = a;
    a =  tem + (yield x);
    console.log(3, a)
}

参考

  1. [译] 关于 ES6 生成器 Generator 的探索
  2. MDN generator function(生成器函数)
  3. MDN Generator(生成器对象)
  4. 阮一峰
yaofly2012 commented 5 years ago

深入Generator——异步

都说async/awaitGenerator+Promise的语法糖,通过本文逐步揭开async/await背后的秘密...

一、使用Generator把异步逻辑同步化

function asyncOp(x) {
    return new Promise((resolve, reject) => {
        setTimeout(() => {
            Number.isSafeInteger(x) 
            ? resolve(12)
            : reject(new Error('Invalid integer'));
        }, 3000)
    })
}

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

var g = gen(1);
// 获取异步操作
var asyncAction = g.next();

asyncAction.value
.then(value => {
    // 把异步操作的结果值传给生成器函数
    var result = g.next(value);
    console.log(result.value);
})

整体思路:

  1. 通过yield返回异步操作(并暂停生成器函数执行)【由内到外】;
  2. 通过next方法把异步操作的结果值传入生成器函数(并继续执行生成器函数)【由外到内】; 相对于生成器函数里代码来说并不关心yield表达式的值是同步还是异步。
  3. 如果异步操作失败了,可以跟通过throw方法在暂定位置抛异常。
    
    var g = gen('a');
    var asyncAction = g.next();

asyncAction.value .catch(reason => { // 通过throw告诉生成器函数异常操作发生了异常 g.throw(reason); })


# 二、实践:异步加法
```js
function getRadom() {
  return (Math.random() * 100) >>> 0;
}

function getRandomAsync() {
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(getRadom());
    }, 2000)
  })
}

function* sum() {
  var x = yield getRandomAsync();
  console.log(`x=${x}`)
  var y = yield getRandomAsync();
  console.log(`y=${y}`)
  return x + y;
}

1. 最搓的方式:逐步调用

var gen = sum();
// 获取异步操作1
gen.next().value
.then(val => {
  // 把异步操作1的结果传给生成器函数,并获取异步操作2
  gen.next(val).value
  .then(val => {
    // 把异步操作2的结果传给生成器函数,并获最终结果
    var sum = gen.next(val).value;
    console.log(`sum=${sum}`)
  })
})

上面的写法就像记流水账,如果有3个数相加还这样写岂不是要疯。

2. 固定的模式的调用方式:

function genFunctionRunner(genFunc) {

  return new Promise(function(resolve, reject) {  
    // 创建生成器对象  
    var gen = genFunc();

    // 开启执行
    doRun(); 

    function doRun(val) {
      try {
        var p = gen.next(val);
        // 
        if(p.done) {
          resolve(p.value)
          return;
        }

        Promise.resolve(p.value)
        .then(doRun) // 把上一个异步操作成功结果传给生成器对象,并恢复执行
        .catch(gen.throw) // 把上一个异步操作失败结果传给生成器对象,并恢复执行
      } catch(e) {
        reject(e)
      }      
    }      
  })
}

genFunctionRunner(sum).then(console.log)

注意:

  1. genFunctionRunner函数的关键是自执行和利用递归获取下一个yield返回值。
  2. 这里使用Promise.resolve方法data.value转成Promise,因为Promise.resolve的特殊功能:如果实参value是个Promise对象,则直接返回实参

三、实践:处理异步操作的异常

function runner(genFunc) {
  var gen = genFunc(); 

  return new Promise((resolve, reject) => {
    doRun();

    function doRun(arg) {
      try {
        // 捕获`next`方法抛出的异常
        var data = gen.next(arg);
        if(data.done) {
          return resolve(data.value);
        }

         // 如果还没结束,就等异步操作结束后递归调用doRun。
        return Promise.resolve(data.value)
            .then(doRun)
            .catch(gen.throw) // 通过`throw`方法告诉生成器异常了       
      } catch(error) {
        reject(error);
      }   
    }                  
  })   
}

runner(sum).then(sum => {
  console.log(sum)
})
.catch(reason => {
  console.error(reason)
})
  1. 异常不仅来自throw方法,next方法也可能会抛出异常,所以在最外层使用try-catch捕获next方法抛出的异常;
  2. runner方法的返回值也不再是doRun()了,而改成了Promise,用于处理next方法抛出的异常和最终的结果值。

捕获throw方法抛出的异常也可以采用try-catch方式,这样就跟捕获next方法抛出的异常保持一致了:

function genFunctionRunner(genFunc) {
    return new Promise(function(resolve, reject) {  
        // 创建生成器对象  
        var gen = genFunc();

        // 开启执行
        doRun(); 

        function doRun(method, arg) {
            try {
                var p = gen[method](arg);
                // 
                if(p.done) {
                    resolve(p.value)
                    return;
                }

                Promise.resolve(p.value)
                .then(val => {
                    doRun('next', val);
                }) 
                .catch(reason => {
                    doRun('throw', reason);
                })
            } catch(e) {
                reject(e)
            }      
        }      
    })
}

genFunctionRunner(sum).then(console.log)

四、实践:分析async/awaitGenerator+Promise写法

Babel如何把async转成Generator?

function asyncGeneratorStep(gen, resolve, reject, _next, _throw, key, arg){
    try{
        var info = gen[key](arg);
        var value = info.value;
    }catch(error){
        reject(error);
        return;
    }

    if(info.done){
        resolve(value);
    }else{
        Promise.resolve(value).then(_next, _throw);
    }
}

// 负责把`async`转成`generator`
function _asyncToGenerator(fn) {
    return function () {
        // 处理传给生成器的参数
        var self = this,
            args = arguments;

        return new Promise(function (resolve, reject){
             // 生成器的函数在Promise参数的回调函数里执行,并且处理参数
            var gen = fn.apply(self, args);

            function _next(value){
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "next", value);
            }

            function _throw(err){
                asyncGeneratorStep(gen, resolve, reject, _next, _throw, "throw", err);
            }
            _next(undefined);
        });
    };
}

使用固定的模式把async转成Generator,Babel的实现更严谨些:

  1. 可以传参数给生成器函数;
  2. 先转Generator,调用时再传参;
  3. 递归调用yiled操作的函数没有直接定义在async转生成器的函数里,省得每次调用都生成一个函数(高,实在是高)。

总结下async/await转成Generator函数方式:

  1. await直接替换成yield;
  2. async函数体代码转成生成器代码(匿名的生成器函数);
  3. async函数名被转成普通函数内部调用生成器函数的函数。
async function sum() {
    var x = await 1;
    var y = await 2;
    return x + y;
}

// 对应的生成器方式
function sum() {
    return _sum.apply(this, arguments);
}

function _sum() {
  // 注意:函数体内的_sum变量是个局部变量,不影响外部作用域下的_sum的值。
  _sum = _asyncToGenerator(function* () {
    var x = yield 1;
    var y = yield 2;
    return x + y;
  });

  // 创建生成器对象,并开始执行生成器
  return _sum.apply(this, arguments);
}

参考

  1. Async-Await≈Generators+Promises
yaofly2012 commented 4 years ago

深入Generator——polyfill

yaofly2012 commented 4 years ago

练习

1. 输出结果

var a = 0
function* gen(x) {
    a = a + (yield x);
    console.log(3, a)
}

var g = gen(10)
var r = g.next();
console.log(1, r.value)
a++;
console.log(2, a)
g.next(5);

知识点: