ES6 中的 generator 函数究竟是什么

ES6 中的 generator 函数究竟是什么

我们在学习 js 的时候应该都知道一个概念:一旦函数开始执行,它将运行直至完成,没有其他的代码可以在运行期间干扰它。

但是在 ES6 中引入了一种新型的函数,它不按照"运行至完成"的规则。这种新型的函数称为"generator"。

js 复制代码
var a = 1;

function func() {
    a++;
    func2();
    console.log(a)
}
function func2() {
    a++;
}
func();    
// 3

在上面的 func 函数中,a++ 运行完后会执行 func2() 函数,最终 a 的值为 3。

要是 func2() 不存在,但以某种方式依然可以在 a++ 和 console.log(a) 语句之间运行呢?这可能吗?

在抢占式(preemptive) 多线程语言中,func2() 去"干扰"并正好在两个语句之间那一时刻运行,实质上是可能的。但 JS 不是抢占式的,也不是多线程的。但是,如果 func() 本身可以用某种办法在代码的这一部分指示一个"暂停",那么这种"干扰"(并发)的协作形式就是可能的。

比如:

js 复制代码
var a = 1;
function *func() {
    a++;
    yield;
    console.log(a);
}
function bar() {
    a++
}

我们很可能在大多数其他的JS文档/代码中看到,一个 generator 的声明的格式为 function* func() { .. } 而不是上面的 function *func() { .. },它们之间唯一的区别是*的位置不同。这两种形式在功能性/语法上是完全一样的,还有第三种function*foo() { .. }(没空格)形式。

现在我们来运行一下上面的代码:

js 复制代码
// 构建一个迭代器it来控制generator
let it = func();
a;                // 1
it.next();
a;                // 2
bar();
a;                // 3
it.next();
// 3

看起来很陌生对吧,我们来讲解一下这个过程:

  1. it = func(); func 不是一个普通函数,func() 并不会运行它,而是构建了一个用来控制它执行的迭代器(iterator);
  2. 我们观察 a 的值,还是 1;
  3. it.next() 启动了 func 函数的执行,并且运行到 func 函数的第一行,也就是 a++;之后在 yield 语句暂停,此时第一个 it.next() 调用结束,此时 func 函数还是运行的,不会被垃圾回收机制回收掉,但是它现在处于暂停节点,等待下一次的 next() 方法重新启动;
  4. 再次观察 a 的值,变成 2;说明运行了 a++;
  5. 此时执行 bar() 方法,再次对 a 进行递增;
  6. 最后执行 it.next(),重新从暂停的地方启动函数,执行 console.log(a),把 a 的值打印出来。

generator 是一种函数,它可以开始和停止一次或多次(在遇到 yield 关键词时会暂停,执行 next() 方法启动),甚至没必要一定要完成。

generator 是一个函数,这也就意味着它是可以接受参数及返回值的:

js 复制代码
function *func(x, y) {
    return x * y;
}
// 传参跟普通函数一样
let it = func(1, 2)
it.next()           // {value: 2, done: true}

我们可以很明显发现,虽然 func(1, 2) 这样的传参跟普通函数一样,但是它实际上并不会执行,我们只是创建了迭代器对象,将它赋值给变量 it,当我们调用 it.next() 时,它指示 func(...) 从现在的位置向前推进,直到遇到一个 yield 或者到函数的最后。

next(...) 调用的结果是一个带有 value 属性的对象,它持有从 func(...) 返回的任何值(如果有的话)。换句话说,yield 导致在 generator 运行期间,一个值被从中发送出来,有点儿像一个中间的 return。

generator 除了接收参数和拥有返回值,它们还内置有更强大,更吸引人的输入/输出消息能力,这是通过使用 yield 和 next(...) 实现的:

js 复制代码
function *func(x) {
	let y = x * (yield);
	return y;
}

let it = func( 1 );

// 开始执行 func(..)
it.next();          // {value: undefined, done: false}

it.next( 2 );       // {value: 2, done: true}

it = func(1) 中将 1 作为参数 x 传入。之后调用 it.next() 开始启动 func()

之后在 func 内部,开始执行 let y = x * (yield),但是它遇到 yield 后暂停了,在下一个 next 方法中传递了一个值 2,此时 2 作为 yield 的结果。因此,赋值语句实际上是 let y = 1 * 2,最后把 y return 出去,作为 next() 方法的结果。

从上面的几个实例中我们可以看到,next 总是比 yield 多一个,因为第一次 next 是用于启动 generator 的,之后的其他 next 才会跟 yield 相对应。

除了可以通过 next 给 yield 传值外,还可以通过 yield 给 next 的结果赋值:

js 复制代码
function *func(x) {
    let y = x * (yield "leo");
    return y;
}

let it = func(1);
it.next();         // {value: 'leo', done: false}

it.next(2);        // {value: 2, done: true}

因为只有一个暂停的 yield 才能接收这样一个被 next(...) 传递的值,但是当我们调用第一个 next() 时,在generator 的最开始并没有任何暂停的 yield 可以接收这样的值。语言规范和所有兼容此语言规范的浏览器只会忽略任何传入第一个 next() 的值。传递这样的值是一个坏主意,因为我们只不过创建了一些令人困惑的代码。所以,我们要记得总是用一个无参数的 next() 来启动 generator。

迭代器

一个 generator 本身在技术上讲并不是一个 iterable,但是当我们执行 generator 时,我们就能得到一个迭代器:

我们写一个无限数字序列生成器:

js 复制代码
function *genNumber() {
    let nextNumber;
    while(true) {
        if(nextNumber === undefined) {
            nextNumber = 1;
        } else {
            nextNumber += 1;
        }
        yield nextNumber;
    }
}

通常来说在一个真实的 JS 程序中含有一个 while...true 循环通常是一件非常不好的事情,如果它没有一个 break 或 return 语句,那么它就很可能永远运行,并同步阻塞/锁定浏览器 UI。但是在 generator 函数中,如果循环中含有 yield,那它就是完全没有问题的,因为 generator 将在每次迭代后暂停,可以重新回到主程序或事件循环队列中。

js 复制代码
let gen = genNumber();
for (var v of gen) {
	console.log( v );

	// 停止循环
	if (v > 500) {
		break;
	}
}

genNumber 是一个 generator 函数,调用这个 generator 可以生成一个迭代器给 for...of 使用,这个迭代器中有一个 Symbol.iterator 函数,会把 next 方法中的 value 读取出来。

而在循环中的 break 被调用后,func 实例基本上被留在了一个永远挂起的状态。

js 复制代码
gen           // genNumber {<closed>}
gen.next();   // {value: undefined, done: true}

for...of 循环的"异常完成"(或者叫做 "提前终结"),一般是由break,return,或未捕捉的异常导致的------会向 generator 的迭代器发送一个信号,以使它终结。技术上讲,for...of 循环也会在循环正常完成时向迭代器发送这个信号。

虽然一个 for...of 循环将会自动发送这种信号,但是我们也可以通过调用 generator 的 return() 方法来手动发送:

js 复制代码
let gen = genNumber();
for (var v of gen) {
	console.log( v );

	// 停止循环
	if (v > 500) {
		console.log(gen.return('停止循环'))    // {value: '停止循环', done: true}
	}
}

在 generator 中还有个特性,如果在内部指定一个 try...finally ,它将总是被执行,即便是 generator 从外部被完成。

js 复制代码
function* genNumber() {
    try {
        let nextNumber;
        while (true) {
            if (nextNumber === undefined) {
                nextNumber = 1;
            } else {
                nextNumber += 1;
            }
            yield nextNumber;
        }
    } finally {
        console.log('finally');
    }
}
let gen = genNumber();
for (var v of gen) {
	console.log( v );

	// 停止循环
	if (v > 500) {
		console.log(gen.return('停止循环'))
	}
}
// ...
// finally
// {value: '停止循环', done: true}

从上面的打印可以看到,在执行 generator 的 return 方法后,会先触发内部的 finally 块(如果它存在的话),之后才打印出 return 的返回结果(返回的 value 设置为传入 return(...) 的任何值)。我们现在也不必再包含一个 break,因为 generator 的迭代器会被设置为 done:true,所以 for...of 循环会在下一次迭代时终结。

generator 处理异步

我们先来看一段代码:

js 复制代码
function foo(x,y,cb) {
	ajax(
		"http://xxxx?x=" + x + "&y=" + y,
		cb
	);
}
foo( 1, 2, function(err,text) {
	if (err) {
		console.error( err );
	}
	else {
		console.log( text );
	}
});

使用 generator 实现相同的逻辑:

js 复制代码
function foo(x,y,cb) {
	ajax(
		"http://xxxx?x=" + x + "&y=" + y,
		function (err, data) {
            if(err) {
                it.throw(err);
            } else [
                it.next(data)
            ]
        }
	);
}

function *main() {
    try {
        let data = yield foo(1 ,2);
        console.log(data);
    } catch(err) {
        console.log(err);
    }
}

var it = main();
it.next();

在generator内部的代码看起来完全是同步的(除了yield关键字本身),但实际上在 foo(...) 内部,操作可以完全是异步的。

除了写法上看起来跟同步的一样外,它还可以使用 try...catch 捕获。

看上面的代码,在 yield 之后,我们在 foo 函数内部使用 it.throw 抛出一个错误,使得这个错误被传递给 yield,最终被 try...catch 捕获。同样,也可以通过 yield 把错误传递给 next:

js 复制代码
function *main() {
	var x = yield "leo";

	yield x.toLowerCase();	// 引发一个异常
}

var it = main();

it.next().value;			// leo

try {
    // 给 yield 赋值为 2,导致 x 没有 toLowerCase 方法
	it.next( 42 ); 
}
catch (err) {
	console.error( err );	// TypeError
}

与 Promise 结合

在 async/await 出现之前,最有意思的就将 generator 与 Promise 进行结合使用:

js 复制代码
function sendRequest(x, y) {
    return request('http:/xxx/?x=' + x + '&y=' + y);
}

function *main() {
    try {
        let data = yield sendRequest(1, 2);
        console.log(data);
    } catch(err) {
        console.log(err)
    }
}

let it = main();
let p = it.next().value;
p.then(
    function fulfilled(data) {
        it.next(data);
    },
    function rejected(err) {
        it.throw(err);
    }
)

在 async/await 出现之后,我们就有更简便的写法了:

js 复制代码
async function main() {
    try {
        let data = await sendRequest(1, 2);
        console.log(data);
    } catch(err) {
        console.log(err);
    }
}
相关推荐
百万蹄蹄向前冲30 分钟前
Trae分析Phaser.js游戏《洋葱头捡星星》
前端·游戏开发·trae
朝阳5811 小时前
在浏览器端使用 xml2js 遇到的报错及解决方法
前端
GIS之路1 小时前
GeoTools 读取影像元数据
前端
ssshooter2 小时前
VSCode 自带的 TS 版本可能跟项目TS 版本不一样
前端·面试·typescript
你的人类朋友2 小时前
【Node.js】什么是Node.js
javascript·后端·node.js
Jerry2 小时前
Jetpack Compose 中的状态
前端
dae bal3 小时前
关于RSA和AES加密
前端·vue.js
柳杉3 小时前
使用three.js搭建3d隧道监测-2
前端·javascript·数据可视化
lynn8570_blog4 小时前
低端设备加载webp ANR
前端·算法
LKAI.4 小时前
传统方式部署(RuoYi-Cloud)微服务
java·linux·前端·后端·微服务·node.js·ruoyi