生成器(Generator)
目录:
概念
通过一段程序,持续迭代或枚举出符合某个公式或算法的有序数列中的元素。
function* fibo(){
let a = 0;
let b = 1;
yield a;
yield b;
while(true){
let next = a + b;
a = b;
b = next;
yield next;
}
}
let generator = fibo();
for(let i = 0; i < 10; i++){
console.log(generator.next().value); // 0 1 1 2 3 5 8 13 21 34
}
生成器函数 (Generator Function)
生成器函数和普通函数的语法差别在于,在 function 语句之后和函数名之前有一个“*”作为生成器函数的标示符。
function* fibo(){
// ...
}
生成器函数也可以使用表达式进行定义:
const fnName = function* (){ /* ... */ }
yield 语句的作用与 return 语句有些相似,但并非退出函数体,而是切出当前函数的运行时(此处为一个类的协程,Semi-coroutime),与此同时可以将一个值(可以是任何类型)带到主线程中。
生成器 (Generator)
生成器是一种类协程或半协程(Semi-coroutine),它提供了一种可以通过特定语句或方法使其执行对象(Execution)暂停的功能,而这语句一般都是 yield 语句。
在 ES6 中,yield 语句可以将一个值带出协程,而主线程也可以通过生成器对象的方法将一个值带回生成器的执行对象中去。
const inputValue = yield outputValue;
生成器切出执行对象并带出 outputValue ,主线程经过同步或异步处理后,通过 .next(val) 方法将 inputValue 带回生成器的执行对象中。
使用方法
构建生成器函数
function* genFn(){
let a = 2;
yield a;
// 为了使生成器能够根据公式不断输出数列元素所以指定条件为 true 保持程序不断执行
while(true){
yield a = a / (2 * a + 1);
}
}
在定义首项为2之后,首先将首项通过 yield 作为第一个值切出,其后通过循环和公式将每一项输出。
启动生成器
生成器函数不能直接作为普通函数来使用,因为在调用时无法直接执行其中的逻辑代码。执行生成器函数会返回一个生成器对象,用于运行生成器内容和接受其中的值。
const gen = genFn();
生成器是通过生成器函数实现的一个生成器(类)实例。
伪代码:
class Generator{
next(value)
throw(error)
[@@iterator]()
}
操作方法(语法) | 方法内容 |
---|---|
generator.next(value) | 获取下一个生成器切出状态(第一次执行时为第一个切出状态) |
generator.throw(error) | 向当前生成器执行对象抛出一个错误,并终止生成器的运行 |
generator[@@iterator] | @@iterator即 Symbol.iterator,为生成器提供实现可迭代对象的方法,使其可以直接被 for-of 循环语句使用 |
其中 .next(value) 方法会返回一个状态对象,包含当前生成器的运行状态和所返回的值。
{
value: Any,
done: Boolean
}
生成器执行对象会不断检查生成器的状态,一旦遇到生成器内的最后一个 yield 语句或第一个 return 语句便会进入终止状态,即状态对象中的 done 属性会从 false 变为 true。
而 .throw(error) 方法会提前让生成器进入终止状态,并将 error 作为错误抛出。
运行生成器内容
生成器对象自身也是一种可迭代对象,我们直接使用 for-of 循环便可遍历。
for(const a of gen){
if(a < 1/100) break;
console.log(a);
}
// 2
// 0.4
// 0.22222222222222224
// ....
深入理解
运行模式
生成器函数以及生成器对象的检测
function* genFn(){
let a = 2;
yield a;
while(true){
yield a = a / (2 * a + 1);
}
}
const gen = genFn();
console.log(genFn().constructor.prototype); // GeneratorFunction
console.log(gen.constructor.prototype); // Generator
// 检测生成器函数
function isGeneratorFunction(fn){
const genFn = (function* (){}).constructor;
return fn instanceof genFn;
}
console.log(isGeneratorFunction(genFn)); // true
// 检测生成器实例对象
function isGenerator(obj){
return obj.toString ? obj.toString() === '[object Generator]' : false;
}
console.log(isGenerator(gen)); // true
console.log(isGenerator({})); // false
使用@@toStringTag 属性来对生成器函数进行类型检测。
// 优化版
function isGeneratorFunction(fn){
// If the current engine supports Symbol and @@toStringTag
if(Symbol && Symbol.toStringTag){
return fn[Symbol.toStringTag] === 'GeneratorFunction';
}
// Using instanceof statement for detecting
const genFn = (function* (){}).constructor;
return fn instanceof genFn;
}
新语法 yield*
它和 yield 的区别在于, yield* 的功能是将一个生成器对象嵌套在另一个生成器内,并将其展开。
function* foo(){
yield 1;
yield 2;
}
function* bar(){
yield* foo();
yield 3;
yield 4;
}
for(const n of bar()) console.log(n);
// 1
// 2
// 3
// 4
生成器与协程
为了实现以生成器作为逻辑执行主体,把异步方法带到主线程去,就要先将异步函数做一层包装,使得其可以在带出生成器执行对象之后再执行。
// Before
function echo(content, callback){
callback(null, content);
}
// After
function echo(content){
return callback => {
callback(null, content);
}
}
将方法带出生成器执行对象后,还需要在主线程将带出的函数执行。上面我们通过封装所得到的异步方法再生成器内部执行后,可以通过 yield 语句将内层的函数带到主线程中,这样就可以在主线程中执行这个函数并得到返回值,然后将其返回到生成器执行对象中。
function run(genFn){
const gen = genFn();
const next = value => {
const ret = gen.next(value);
if(ret.done) return;
ret.value((err, val) => {
if(err) return console.error(err);
// Loop
next(val);
})
}
// First call
next();
}
run(function* (){
const msg1 = yield echo('Hello');
const msg2 = yield echo(`${msg1} World`);
console.log(msg2); // Hello World
})