generator又名生成器函数,它是一个崭新的函数类型,它和标准的普通函数完全不同。通过显式的调用生成器函数,能对应的产生一个新的值。通过多次调用后,产生一组值的序列,直到生成器告诉我们无法在产生新的值了。每当生成器函数产生一个新值后,它的执行状态会被保留,直到下次请求到来,它就会从上次离开的位置恢复执行。
1、如何定义generator函数
下面我们来看一个简单的例子:
// 通过在function后面添加星号*来定义生成器函数
function* myGenerator() {
// 使用关键字yield产生值
yield "first";
yield "second";
yield "third";
}
// 生成迭代器gen
let gen = myGenerator();
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
console.log(gen.next());
上述例子首先定义了一个生成器函数,它能够产生一系列的值。创建生成器函数非常简单,只要在function后面加上一个星号(*)。然后只要在函数体内部使用关键字yield,就能产生值了。
要使用生成器函数,需要先对它进行调用,并生成迭代器gen。
let gen = myGenerator();
之后每当你想要获取一组新的值时,调用gen.next()方法即可。gen.next这个方法会执行函数内部代码,直到遇到yield为止,停止执行,并将yield后面的值返回。返回格式为{ value, done },其中value表示本次yield的值,done表示生成器函数是否执行完成。从上面的输出结果可以看出,从第四次开始(包括第四次),value就为undefined,done为true,表示生成器已经执行完成,之后再调用gen.next()方法输出的都是相同的结果。
2、如何和generator函数进行交互
从上述的例子可以看到,我们可以通过yield来从生成器中产生多个值并返回。但是生成器的功能远不止如此,我们还可以向生成器发送值。当我们调用next方法获取一个值时,我们可以对值进行一系列的操作,然后在处理完成时,再把计算结果传递给生成器。
下面我们来看个简单的例子:
// 生成器函数可以接受参数
function* myGenerator(msg) {
const resultMsg = yield ("first & " + msg);
console.log(resultMsg); // world
yield ("second & " + resultMsg + " & " + msg);
}
// 调用生成器函数时传递参数,并生成迭代器gen
const gen = myGenerator("hello");
const result1 = gen.next();
console.log(result1); // {value: "first & hello", done: false}
const result2 = gen.next("world");
console.log(result2); // {value: "second & world & hello", done: false}
一开始我们定义了一个生成器函数,并定义了一个参数msg。在调用生成器时,传递了'hello'这个参数给生成器。
const gen = myGenerator("hello");
之后我们调用gen.next方法获取生成器产生的值(yield),打印出result1发现,这个返回值能获取到我们一开始传入的参数msg。
const result1 = gen.next();
console.log(result1); // {value: "first & hello", done: false}
此时生成器内部执行到的位置为
接着我们继续调用gen.next方法,并同时往方法内部传递了参数''world",这个值会作为yield的返回值赋值给生成器函数内部的resultMsg这个变量。通过打印的结果可以看到resultMsg为"world"。
const result2 = gen.next("world");
console.log(result2); // {value: "second & world & hello", done: false}
此时生成器内部执行到的位置为
以上实例展示了如何在生成器函数中实现双向通信。我们通过yield语句从生成器函数中返回值,再使用迭代器的next()方法把值传回生成器。
3、generator函数的异常处理
还有一种稍微不那么正统的方式将值应用到生成器上:通过抛出一 个异常。每个迭代器除了有一个next方法,还有一个throw方法,让我们 再来看一个简单的例子。
function* myGenerator() {
try {
yield "first";
throw new Error('error in generator');
} catch (e) {
console.log(e === 'out error'); // true
}
}
const gen = myGenerator();
const result = gen.next();
console.log(result.value); // 'first'
// 向生成器抛出一个异常
gen.throw('out error');
例子还是正常的生成器函数的定义,但是生成器函数的内部使用了try / catch来捕获异常。当函数执行到这里时
console.log(result.value); // 'first'
此时生成器内部代码执行到如下位置,还未执行到throw new Error。
接着我们执行了如下代码
// 向生成器抛出一个异常
gen.throw('out error');
通过gen.throw方法往生成器中抛出了一个异常,此时生成器函数恢复,因为此时存在外部传入的异常,所以生成器内部直接进入到catch内部,不会再次执行throw new Error('')这句代码了。如下
通过catch内部打印的结果也能进行印证。
4、generator函数的执行上下文环境
在这一节让我们来聊聊生成器函数如何跟随执行上下文环境的。如果对执行上下文还不是很了解,可以看看博主之前的文章:浅谈js中的闭包、作用域和执行上下文
我们先来看个简单的例子:
function* myGenerator() {
yield 'first';
yield 'second';
}
const gen = myGenerator();
const result1 = gen.next();
const result2 = gen.next();
const result3 = gen.next();
下面这种图简单说明了执行过程:
接着让我们用另外一个例子来深入探讨下生成器函数的执行上下文吧。
function* myGenerator(msg) {
yield "first " + msg;
return "second " + msg;
}
const gen = myGenerator("hello");
const result1 = gen.next();
const result2 = gen.next();
在调用生成器函数myGenerator之前,执行上下文如下:
当我们执行const gen = myGenerator("hello")时,生成器进入挂起开始状态,执行上下文如下:
和普通的函数不同,当函数执行完成后,它当执行上下文从执行栈中弹出后,不会立即被销毁。因为此时gen还保留着对它的引用,可以看成类似闭包的现象。闭包中为了保证闭包创建时的变量都可以使用,需要对创建它对环境保存一个引用。而生成器除了保持环境引用外,还需要保证可以恢复执行,所以需要保存当时函数的执行上下文。
当执行完const gen = myGenerator("hello")后,执行上下文如下:
当我们执行const result1 = gen.next()时,这时会激活myGenerator的执行上下文,并推入执行栈中,执行上下文如下:
当const result1 = gen.next()执行完成后,从栈中推出执行上下文,如下:
最后,当我们执行const result2 = gen.next() 时,又会进入上下文的入栈出栈流程。此时遇到return后,生成器进入完成状态。
5、generator函数的使用
1、使用for...of遍历生成器
function *myGenerator() {
yield 'first';
yield 'second';
}
for (let item of myGenerator()) {
console.log(item);
}
// 输出结果:
// first
// second
上述代码同样可以用while来实现,可以看成for...of实际上就是while的语法糖。
function *myGenerator() {
yield 'first';
yield 'second';
}
let gen = myGenerator();
let item;
while(!(item = gen.next()).done) {
console.log(item.value);
}
// 输出结果:
// first
// second
2、在生成器函数内部调用生成器
function* firstGenerator() {
yield "first";
// yield*将执行权交给了另一个生成器
yield* secondGenerator();
}
function* secondGenerator() {
yield "second";
yield "third";
}
for (let item of firstGenerator()){
console.log(item);
}
// 执行结果
// first
// second
// third
3、结合promise,实现async函数
我们正常使用promise函数进行链式调用时,方式如下:
getJSON("data/first.json")
.then(result1 => getJSON(result1[0].url))
.then(result2 => getJSON(result2[0].url))
.then(result3 => console.log(result3))
.catch(error => console.log("error"));
如果想在代码中实现类似同步代码的风格,如下:
try {
const result1 = getJSON("data/first.json");
const result2 = getJSON(result1[0].url);
const result3 = getJSON(result2[0].url);
console.log(result3);
} catch (e) {
console.log("error");
}
为了达到上述目的,我们改造下代码:
async(function*() {
try {
const result1 = yield getJSON("data/first.json");
const result2 = yield getJSON(result1[0].url);
const result3 = yield getJSON(result2[0].url);
console.log(result3);
} catch (e) {
console.log("error");
}
});
function async(generator) {
// 创建一个迭代器
const gen = generator();
// generator执行顺序控制器
function next(arg) {
const result = gen.next(arg);
// 如果已经结束,则直接return
if (result.done) return;
const value = result.value;
// 如果是Promise则在then里执行next
if (value instanceof Promise) {
value().then(res => next(res))
.catch(err => gen.throw(err))
}
}
try {
next();
} catch (e) {
gen.throw(err);
}
}
上面代码中的async函数实现了promise的同步代码编写风格。了解async和await的同学肯定发现,上述代码有点类似这两个关键字的使用,其实上面的async函数就是对其的简易实现,async和await就是上述函数的语法糖。
6、小结
生成器函数不会在同时输出所有值的序列,而是基于每次请求生成对应的值,并在此过程中挂起和恢复执行状态。
希望本文对你有所帮助。