Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generator函数 #14

Open
yaofly2012 opened this issue Oct 15, 2018 · 3 comments
Open

Generator函数 #14

yaofly2012 opened this issue Oct 15, 2018 · 3 comments

Comments

@yaofly2012
Copy link
Owner

yaofly2012 commented Oct 15, 2018

一、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
  1. 函数表达式,匿名函数都可以定义生成器函数;
  2. 生成器函数可以作为对象和类的成员方法,但是不可以作为
  • 构造函数
    构造函数有自己的返回值逻辑,并且构造函数是解决继承写法问题。虽然不能作为构造函数,但生成器函数也是有prototype属性的。
  • 箭头函数
    没有function关键字。

1.2 特性

  1. 生成器函数可以终止,可以继续执行(反人类);
    再次执行时上下文跟上次暂停时一样。
  2. 生成器函数是“惰性的”,当调用生成器函数的时候不会立马执行,而是等继续(next/throw/return)执行时才执行相关代码,遇到下一个yield表达式时暂停(或者return/throw语句结束);
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

  • 可以简单理解为yield关键字把生成器函数分割成一段段可执行代码片段,调用next方法时就执行其中的代码片段
  • returnthrow方法会立即终止,不会再次执行。
  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}
  1. 如果生成器函数发生异常,则next方法直接向外抛:
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

  • 相当于执行next方法内部发生了异常;
  • 并且记录了错误栈信息
  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*相当于:

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类型类似,并没有定义全局对象GeneratorFunctionGenerator
  2. 生成器函数内部的yield表达式和return语句用于定义生成器对象的内部状态;
  3. 生成器对象既是可迭代对象也是迭代器(其next方法符合迭代器协议)。

2.2 遍历

生成器对象本身就是个可迭代对象,但是只能一次性遍历。

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)

  • next方法用于获取生成器对象下一个状态,即 yield后面表达式的值
  • 参数value可以指定生成器函数里 yield表达式的值 (注意区分“yield后面表达式的值”和yield表达式本身的值
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},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)

让生成器函数在上次暂停的地方(或者函数开始处)立马结束,并返回指定的状态值(return方法实参):

{
  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}
  1. 立马结束和finally语句块必须执行的平衡
    try-finally语句块中finally语句块中语句必须要执行,这跟return方法立马结束有点冲突。
    当调用return方法时,如果上个yield表达式在try语句块中则return方法会推迟到finally代码块执行完再执行,并且可以修改返回值。即保证了finally的语法规则不变(一定要执行,可以修改try/catch中的返回值)。
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处抛一个异常,也说明了函数开始处抛出的异常无法内部捕获。

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 yaofly2012 changed the title ES7:async/await ES7:async/await, generator, iterator Nov 2, 2018
@yaofly2012 yaofly2012 changed the title ES7:async/await, generator, iterator async/await, generator, iterator Nov 2, 2018
@yaofly2012
Copy link
Owner Author

yaofly2012 commented Jan 16, 2019

深入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);
})

二、实践:异步加法

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 yaofly2012 added the JS label Feb 12, 2020
@yaofly2012 yaofly2012 changed the title async/await, generator, iterator JS-异步-generator, async/await Feb 17, 2020
@yaofly2012 yaofly2012 changed the title JS-异步-generator, async/await JS-ES6-ESnext-generator, async/await Feb 21, 2020
@yaofly2012
Copy link
Owner Author

yaofly2012 commented Feb 22, 2020

深入Generator——polyfill

@yaofly2012 yaofly2012 changed the title JS-ES6-ESnext-generator, async/await JS-ES6-ESnext-generator函数 Feb 22, 2020
@yaofly2012
Copy link
Owner Author

练习

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);

知识点:

  • 生成器函数可以保持上下文状态的情况下暂停执行。

@yaofly2012 yaofly2012 changed the title JS-ES6-ESnext-generator函数 Generator函数 Nov 12, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant