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);
    }
}
相关推荐
小镇程序员11 分钟前
vue2 src_Todolist全局总线事件版本
前端·javascript·vue.js
野槐13 分钟前
前端图像处理(一)
前端
程序猿阿伟20 分钟前
《智能指针频繁创建销毁:程序性能的“隐形杀手”》
java·开发语言·前端
疯狂的沙粒22 分钟前
对 TypeScript 中函数如何更好的理解及使用?与 JavaScript 函数有哪些区别?
前端·javascript·typescript
瑞雨溪30 分钟前
AJAX的基本使用
前端·javascript·ajax
力透键背33 分钟前
display: none和visibility: hidden的区别
开发语言·前端·javascript
程楠楠&M44 分钟前
node.js第三方Express 框架
前端·javascript·node.js·express
weiabc44 分钟前
学习electron
javascript·学习·electron
盛夏绽放1 小时前
Node.js 和 Socket.IO 实现实时通信
前端·后端·websocket·node.js
想自律的露西西★1 小时前
用el-scrollbar实现滚动条,拖动滚动条可以滚动,但是通过鼠标滑轮却无效
前端·javascript·css·vue.js·elementui·前端框架·html5