阅读 307

Generator,async/await究竟是什么?(一.Generator)

Promise那一章节中,我提到了async/awaitgenerator。答应大家会更新相关内容,今天它来了。

Generator函数

在聊async/await前,必须先聊一下generator,因为async/awaitgenerator的语法糖。
前端人员都知道,generator有一下几个特点:

  • function关键字与函数名之间有一个星号 "*" 。
  • 函数体内使用 yield 表达式,定义不同的内部状态 (可以有多个yield)。
  • 直接调用 Generator函数并不会执行,也不会返回运行结果,而是返回一个遍历器对象(Iterator Object)。
  • 依次调用遍历器对象的next方法,遍历 Generator函数内部的每一个状态。

仅仅知道这些我认为还不够,我们先来看一下,声明完这个函数后,这个函数中到底有什么东西: image.png

  1. 首先我们在prototype下的__proto__中找到了挂载在实例下的方法next()return()throw()。这些方法我们肯定是要搞懂的。继续向下看。
  2. 普通函数的__proto__下面的constructor都是Function(),说明是被Function实例化出的对象,但它的是 GeneratorFunction()。这是个问题。

通过这张图,目前发现这两个问题,我们先解决第一个,但在这之前我还有做一个简单的介绍作为基础。
在普通函数中,我们想要一个函数最终的执行结果,一般都是return出来,或者以return作为结束函数的标准。运行函数时也不能被打断,期间也不能从外部再传入值到函数体内。
但在 Generator 中就打破了这几点,所以他与其他函数完全不同。
当以function*的方式声明了一个 Generator生成器时,内部是可以有许多状态的,以yield进行断点间隔。期间我们执行调用这个生成的 Generator,他会返回一个遍历器对象,用这个对象上的方法,实现获得一个yield后面输出的结果。

function* generator() {
    yield 1
    yield 2
};
let iterator = generator();
iterator.next()  // {value: 1, done: false}
iterator.next()  // {value: 2, done: false}
iterator.next()  // {value: undefined, done: true}
复制代码

Generator实例上的方法

next()

1)从上面代码中,我们也大概了解到.next()其实是为了遍历这个遍历器对象,每一次的调用,都会返回一个对象:

  1. value值为内部yield表达式的返回值。
  2. 还有一个表示是否执行完毕的值donenext()找不到后面的yield关键字时,返回值为true,同时由于没找到最后一个yield表达式的值,所以valueundefined
  3. 如果在yield之前执行了return,那么遍历到return时就会返回{value: undefined, done: true}无论之后再遍历,也不会执行后的热yield

2)我们还可以向next()中传递参数,可以作为下次输出的内容。

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

var g = gen(5); 
g.next() // { value:6, done:false } 
g.next(15) // { value:10, done:false } 
g.next(20) // { value:55, done:true }
复制代码
  • 第一运行用next方法,系统自动忽略参数,返回x+1的值6;
  • 第二次调用next方法,将上一次yield表达式的值设为15,因此y等于30,返回y / 3的值10;
  • 第三次调用next方法,将上一次yield表达式的值设为20,因此z等于20,这时x等于5,y等于30,所以return语句的值等于55。

可以看出该参数会被当做上一条yield语句的返回值。
3)由于for...of是用于迭代可迭代对象的循环方法,所以同样适用于我们的generator返回的遍历器对象。此时就不需要调用next
同理相同性质的方法也是可以遍历出遍历器对象的值,如扩展运算符Array.from().

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

var g = gen(); 
for (let item of g) { 
    console.log(item) // 1, 2, 3
}
[...g]  // [1,2,3]
Array.from(g)  // [1,2,3]
复制代码

return()

无论 generator 中有对少个 yield,只要被调用了 return() 函数后,返回的结果的 done 值被立即置为 true,结束遍历 Generator 函数。
如果 return() 函数不传参数时,valueundifined,传递参数后,value 值为传递的参数。

function* generator() {
    yield 1
    yield 2
    yield 3
    yield 4
}
let g = generator()
console.log(g.next()); // {value: 1, done: false}
console.log(g.return()); // {value: undefined, done: true}
console.log(g.next(5)); // {value: undefined, done: true}
console.log(g.return(new Date())) // {value: Mon Sep 13 2021 23:37:57 GMT+0800 (中国标准时间), done: true}
console.log(g.next()); // {value: undefined, done: true}
复制代码

throw()

调用了 throw() 函数后,Generator 函数内部可以通过 try/catch 来捕获此错误。并返回的结果的 done 值被立即置为 true,结束遍历 Generator 函数。
如果 throw() 函数不传参数时,捕获的异常为 undifined,传递参数后,捕获的异常为传递的参数。一般实例化 Error() 配合使用

function* gen() {
    try {
        yield 1;
        yield 2;
        yield 3;
        yield 4;
    } catch (e) {
        console.log("generator内部错误", e);
    }
}

var g = gen();
console.log(g.next()); // {value: 1, done: false}
console.log(g.next()); // {value: 2, done: false}
try {
    g.throw(new Error("Something went wrong")); // generator内部错误 Error: Something went wrong
    g.throw(new Error("oh no")); 
} catch (error) {
    console.log('外部错误', error); // 外部错误 Error: oh no
}
console.log(g.next());  // {value: undefined, done: true}
复制代码

遍历器对象抛出了两个错误,第一个被 Generator 函数内部捕获。
第二个因为函数体内部的catch 函数已经执行过了,不会再捕获这个错误,所以这个错误就抛出 Generator 函数体,被函数体外的catch 捕获。 之后再执行 next() 方法也不会再继续向下遍历了。

Generator内部是如何被实例化出来的

其实我们无论是声明一个数组、字符串、方法,其实内部都是通过实例化对应构造函数而生成的。

image.png

但这不能一概而论,还是有差别的,回头可以一起聊聊定义基础数据类型发生了什么,new Function 和函数声明的区别,基础和引用数据类型在内存的存储。
这里我们为了引出generator函数是如何被实例化出来的?在控制台打印GeneratorFunction也没有对应的构造函数,说明不在window上。于是使用获取原型对象的方法,来找找他的原型对象上的构造函数。

console.dir(Object.getPrototypeOf(function*(){}).constructor)
// 打印
1.  GeneratorFunction()
1.  1.  arguments: [Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at Function.invokeGetter (<anonymous>:2:14)]
    1.  caller: [Exception: TypeError: 'caller', 'callee', and 'arguments' properties may not be accessed on strict mode functions or the arguments objects for calls to them at Function.invokeGetter (<anonymous>:2:14)]
    1.  length: 1
    1.  name: "GeneratorFunction"
    1.  prototype: GeneratorFunction
    1.  1.  arguments: (...)
        1.  caller: (...)
        1.  constructor: ƒ GeneratorFunction()
        1.  prototypeGenerator {constructorGeneratorFunctionnext: ƒ, return: ƒ, throw: ƒ, Symbol(Symbol.toStringTag): "Generator"}
        1.  Symbol(Symbol.toStringTag): "GeneratorFunction"
        1.  __proto__: ƒ ()
    1.  __proto__: ƒ Function()
    1.  [[Scopes]]: Scopes[0]
    
//那么我们验证下找到的构造函数是不是generator的构造函数
function* gen() {
    yield 1
}
console.log(gen instanceof Object.getPrototypeOf(function*(){}).constructor) // true  是的
复制代码

如果找到Object.getPrototypeOf(function*(){}).constructorgenerator的构造函数,那反过来,我们通过实例化构造函数,应该也能生成generator

var GeneratorFunction = Object.getPrototypeOf(function* () { }).constructor;
var g = new GeneratorFunction("num", "yield num * 2");
console.log(g);  //ƒ* anonymous(num) {yield num * 2}
var iterator = g(10);
console.log(iterator.next().value); // 20
console.log(iterator); 
/*__proto__: Generator
*    [[GeneratorLocation]]
*    [[GeneratorStatus]]: "suspended"
*    [[GeneratorFunction]]: ƒ* anonymous(num )
*    [[GeneratorReceiver]]: Window
*    [[Scopes]]: Scopes[2]
*/
    GeneratorStatus 有两个值"suspended""closed" 对应done的truefalse

复制代码

实践证明,通过实例化GeneratorFunction是可以生成一个 Generator 函数对象。

补充:yield* 表达式

通过上述两个问题,我们大概的了解了generator的语法及来源,如果我们想在 Generator 函数中调用另一个Generator函数,就可以通过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* 表达式表示 yield 返回一个遍历器对象,用于在 Generator 函数内部。

Generator函数告一段落,总结一下:

  1. 定义时使用function*作为函数声明方式。
  2. 调用Generator函数时,返回的是一个迭代器对象不是执行结果,内部GeneratorStatussuspended,closed两个状态值,对应是否完成。
  3. 内部可以有多个yield来完成类似普通函数return的功能。
  4. 可以通过API提供的next()方法,或者迭代循环的语句 for...ofArray.from(),展开运算符 ...,对迭代器对象进行输出每个 yield 后的值,next()也可以传递参数作为上一个yield值参与下次运算。API还有return()throw()
  5. GeneratorFunctiongenerator 的构造函数,但没挂载在window上。
  6. yield *表达式可以在一个 Generator函数中返回另一个Generator函数的遍历器对象

如果此文章对您有帮助或启发,那便是我的荣幸

文章分类
前端
文章标签