今天我们要讲的是JavaScript中一个不太常用的Generator语法。我很少看到有人在实际项目开发中使用它。
可能是因为它的语法比较复杂,而且是 async/awiatcan ,所以人们很少使用它。然而,Generatorit 仍然是。
今天我们就从基础开始练习Generator。
Generator介绍
JavaScript GeneratorE6是引入的一种新型函数,可以生成多个值序列,可以暂停和恢复执行,使我们能够更简单、高效地实现迭代器。
如果我们看到一个函数后面跟着一个 * 符号,那么它就是一个 Generatorfunction :
function* myGenerator() {
// Generator function
}
GeneratorFunctions 可以使用yield语句来定义要生成的值的序列。
每当yield语句时,Generator函数就会暂停执行并返回一个包含当前生成值的对象,然后执行流程将暂停,直到下一次调用generator函数。
它的返回值是一个迭代器,可以通过调用 next() 方法来获取下一个生成的值。Generator函数中的所有yield语句都执行完毕后,done属性为true,表示generator函数已经结束(这里的流程描述比较抽象,后面用实际案例来解释会更好)。
GeneratorFunctions 是 Python 首先从 Coroutine 语言中的 coroutine() 概念演变而来,然后引入到 E6 标准中,希望用它来提高 JavaScript 中处理异步编程的能力。
Generator基本语法
GeneratorFunctions 使用 function*the 关键字定义,它可以包含多个yield表达式来控制函数执行的流程:
function* myGenerator() {
yield 1;
yield 2;
yield 3;
}
调用Generator函数并不执行函数内部的代码,而是返回一个迭代器对象,next()通过调用这个对象的方法来执行函数的代码,并返回yield表达式返回的值:
const myGeneratorIterator = myGenerator();
console.log(myGeneratorIterator.next()); // print { value: 1, done: false }
console.log(myGeneratorIterator.next()); // print { value: 2, done: false }
console.log(myGeneratorIterator.next()); // print { value: 3, done: false }
console.log(myGeneratorIterator.next()); // print { value: undefined, done: true }
Generator函数执行过程中,当yieldan表达式时,函数的执行会被挂起,并将表达式的值返回给调用者。
当next()方法时,函数将从中断处继续执行;我们可以在函数中指定返回一个最终返回值,该值将被包装在包含要返回的属性的完成对象中:
function* myGenerator() {
console.log('Start');
yield 1;
console.log('Middle');
yield 2;
console.log('End');
return 'Done';
}
const myGeneratorIterator = myGenerator();
console.log(myGeneratorIterator.next()); // print Start, { value: 1, done: false }
console.log(myGeneratorIterator.next()); // print Middle, { value: 2, done: false }
console.log(myGeneratorIterator.next()); // print End, { value: 'Done', done: true }
生成器的高级使用
yield*表达式
Yield* 允许 Generatorus 调用另一个 Generator 函数或函数内的可迭代对象。
当 Generatora 函数到达yield*表达式时,它会暂停执行并将执行转移到另一个 Generator 函数或可迭代对象。
执行权不会返回到原来的Generatorfunction。
function* foo() {
yield 1;
yield 2;
}
function* bar() {
yield* foo();
yield 3;
}
for (let value of bar()) {
console.log(value); // print 1, 2, 3
}
在这个例子中,表达式inGenerator函数调用该函数并将其迭代结果依次返回给该函数。bar()yield* foo()foo()bar()
数据交互
在Generator函数中,可以使用yield表达式将数据返回给调用者,调用者next()可以通过Generator方法将数据传递给函数。
这使得调用者和 Generator 函数之间能够进行数据交互。
function* foo() {
let x = yield;
yield x * 2;
}
let gen = foo();
gen.next(); // start generator
gen.next(10); // pass 10,print 20
在这个例子中,foo()函数的next()在第一次调用该方法时会停在第一个yield语句处,等待外部传入的数据。
然后,当next()方法时将从外部传入的数据作为yield表达式的值,然后向下执行,直到下一个yield表达式返回数据。
实际用例
异步编程
GeneratorFunctions 还可以用于实现异步编程。可以通过调用next()方法和关键字:yieldPromise来控制函数的执行状态
function* myGenerator() {
const result1 = yield new Promise((resolve) => setTimeout(() => resolve('first'), 1000));
console.log(result1);
const result2 = yield new Promise((resolve) => setTimeout(() => resolve('second'), 2000));
console.log(result2);
const result3 = yield new Promise((resolve) => setTimeout(() => resolve('third'), 3000));
console.log(result3);
}
const generator = myGenerator();
const promise = generator.next().value;
promise.then((result) => generator.next(result).value)
.then((result) => generator.next(result).value)
.then((result) => generator.next(result).value);
看起来是不是和 async/awaitof 角色很相似,下面是两种语法的一些比较:
优势:
控制流程更灵活:可以使用 Generator 函数控制异步操作的执行顺序,多个异步操作可以按顺序执行,每个操作完成后执行下一个操作,控制流程更灵活。
Generator函数的状态可以复用:Generator函数的状态可以保存在对象中,需要的时候函数可以继续执行,并且可以使用保存的状态继续异步操作。
更通用:GeneratorFunctions 可用于处理各种类型的异步操作,包括事件、回调、迭代器和 Promisemore 。
缺点:
更高的代码复杂性:使用 Generator 函数可能会增加代码的复杂性,因为需要额外的代码和处理步骤。
可读性差:对比async/await,Generator函数的语法和代码结构都比较复杂,可读性不如async/await。
控制异步进程
使用 Generatorfunctions 也非常方便。假设有一个需求场景API需要获取,所有数据准备好后进行下一步。
此时可以使用Generator函数让这个异步控制流程更加清晰:
function* fetchAllData() {
const data1 = yield fetch('api1');
const data2 = yield fetch('api2');
const data3 = yield fetch('api3');
return [data1, data2, data3];
}
function run(generator) {
const iterator = generator();
function handle(iteratorResult) {
if (iteratorResult.done) {
return Promise.resolve(iteratorResult.value);
}
return Promise.resolve(iteratorResult.value)
.then(res => handle(iterator.next(res)));
}
return handle(iterator.next());
}
run(fetchAllData).then(data => {
// handle all data
console.log(data);
});
处理大数据可以节省内存
在处理大数据集时,如果一次性将所有数据加载到内存中,会造成内存浪费和程序性能下降。
Generator函数可以用来按需处理数据,将数据一一读取并转换,减少内存占用,提高程序性能。
function* dataGenerator() {
let index = 0;
while (true) {
yield index++;
}
}
function* processData(data, processFn) {
for (let item of data) {
yield processFn(item);
}
}
const data = dataGenerator();
const processedData = processData(data, item => item * 2);
for (let i = 0; i < 500; i++) {
console.log(processedData.next().value);
}
实现状态机
GeneratorFunctions 也可用于实现状态机。状态机是由一组状态和状态之间的转移规则组成的数学模型,可以用来描述系统的行为和状态。
在实际开发中,状态机可以用来处理复杂的业务逻辑,比如表单验证、工作流控制等。
使用Generator函数实现状态机的过程如下:
定义状态机的各种状态,每个状态对应一个 Generatorfunction 。
状态之间的转换是使用 Generatorfunction 的 statements.yield 实现的
调用Generator函数时,使用循环依次执行各个状态,直到状态机完成。
这是 Generator 实现的示例:
function* stateMachine() {
let state = 'start';
while (true) {
switch (state) {
case 'start':
console.log('Enter start state');
state = yield 'start';
break;
case 'middle':
console.log('Enter middle state');
state = yield 'middle';
break;
case 'end':
console.log('Enter end state');
state = yield 'end';
break;
}
}
}
const sm = stateMachine();
console.log(sm.next().value); // Enter start state
console.log(sm.next('middle').value); // Enter middle state
console.log(sm.next('end').value); // Enter end state
最后
Generator与async/await相比,语法更加复杂,需要手动控制执行过程,使用起来相对麻烦。这也是我很少看到Generatorit被使用的原因之一。
这种语法实际上并不像看上去那样简洁易懂。
但是在做一些复杂的控制流和状态机处理的时候还是很有用的,Generator可以让我们的流程更加清晰。