【译】ES6 Generators(4)并发篇

  1. 1. 了解 CSP (Communicating Sequential Processes)
    1. 1.1. 『sequential』 即顺序
    2. 1.2. 『Processes』 即进程
    3. 1.3. 『communicating』 即通信
  2. 2. JS 中的 CSP
    1. 2.1. asynquence 中 CSP 的实现

注意:这篇文章没翻译完,可以先看原文

译注1:此文带着自己的理解,不完全按原文翻译。原文地址

译注2:原文晦涩难懂的地方,尽力做了注释或修饰,方便大家理解。错误之处欢迎各位校验指正。

我们最好的主题是探索前沿的东西,接下来的概念可能会听起来有点懵,但一想到在未来这些东西会大派用场,想想都有点小激动呢!

这篇文章的主题受到 David Nolen @swannodette 的鼓舞,他写了介绍 CSP 的一些文章:

当然,你也可以继续阅读这篇文章,听我娓娓道来并发式生成器的介绍。

我尝试了 Go 语言风格的 CSP API 的实现。当然比我更聪明的同行可能会看到我在这个探索中所遗漏的地方,我会持续不断地探索和尝试,并坚持和你们分享我所发现的东西。

了解 CSP (Communicating Sequential Processes)

CSP 这个概念来自 Tony Hoare的《Communicating Sequential Processes》一书。

这是一个非常深的计算机理论,我并不打算以太多晦涩难懂的计算机专业术语,而是轻松地介绍它。

『sequential』 即顺序

这是描述 ES6 生成器中单线程行为和同步风格代码的另一种方式。

还记得一个生成器的语法么?

1
2
3
4
5
function *main() {
var x = yield 1;
var y = yield x;
var z = yield (y * 2);
}

这里的每一个表达式是按序执行,yield 关键字虽然指明了生成器中断和恢复的地方,但并没有改变生成器函数中从上到下执行的顺序,对吧?

『Processes』 即进程

每一个生成器表现得就像是一个虚拟的进程,它可以自己中断,向其他生成器(进程)传递信号,且能从其他生成器(进程)接收信号后,恢复自己的执行流程。

如果生成器能够访问共享的内存空间的话(也就是能访问除自己内部的本地变量外的自由变量),它就不是那么独立了。

假设我们有一个不访问外部变量的生成器函数,那么它在理论上就可以执行自己的进程。

但我们通常同时有多个生成器(多进程)绑定在一起,需要彼此间协作以完成任务。

那我们为什么要将生成器分离为多个,而不是合在一起呢?因为我们要做到 功能与关注点的分离(separation of capabilities/concerns)

假定我们将 XYZ 任务分离为连续的子任务 X, Y, Z 分别实现,就增加了程序的维护性。举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 原来是这样
function XYZ() {
console.log('x');
console.log('y');
console.log('z');
}
// 可以拆分为:
function X() {
console.log('x');
Y();
}
function Y() {
console.log('y');
Z();
}
function Z() {
console.log('z');
}

将功能进行模块化划分,增大了程序的可维护性。

同理,对于多生成器(多进程)来说,我们也可以这么做。

『communicating』 即通信

生成器(进程)之间互相协作,就需要一个通信频道(communication channel)来传递消息。

实际上,我们并不一定需要在通信频道上传递消息来实现通信,我们可以通过移交控制权的方式来实现。

为什么要通过移交控制权的方式呢?主要是因为 JS 是一个单线程的语言。

单线程意味着同一时刻只能执行一个任务,其他任务排在队列里被挂起(或者说是中断),等待队列前面的任务完成才能恢复自己的执行。

多个独立的生成器(线程)能够协作和通信好像不是很现实,将多个生成器分离以实现松耦合的目标看似美好但好像不切实际。

可能我是错的,但我并没有找到实现任意两个生成器绑定到一起实现 CSP 匹配的方法。要实现这种设计的话,两个生成器或许需要一个通信协议来支撑。

JS 中的 CSP

这里有几个应用于 JS 的 CSP 理论探索。

前面提到的 David Nolen 有几个有趣的项目,包括 Omcore.async

Koa 也有一个有趣的实现,主要是通过它的 use() 方法。

还有一个类似 core.async/Go CSP API 实现的 js-csp

你可以去了解这几个项目用 JS 实现的不同的 CSP。

asynquence 中 CSP 的实现

我已经有 asynquence 的 runner() 插件来处理异步的生成器操作,所以我在这里尝试实现了 CSP 功能。

我需要解决的第一个问题是:我们怎么知道哪一个生成器即将接管控制权呢?

我们可以让每一个生成器都有一些特定的属性如 ID 来告知其他生成器的话,这样做好像比较繁琐笨重。

在经过各种实验后,我选择了一种循环调度的方法:如果我们要将 A, B, C 三个生成器连接起来,且 A 会得到控制权,接着 A 发出 yield 信号将控制权移交给 B, 再接着 B 发出 yield 信号将控制权移交给 C,最后 C 再把控制权移交给 A,形成一个循环。

但我们怎么精确地实现控制呢?有明确的 API 么?同样,在经过很多实验后,我使用了一个巧妙的办法,与 Koa 中实现的类似

每一个生成器都有一个指向共享的令牌(token),对这个令牌 yield 后就会发出一个移交控制的信号。

未完待续。。。http://davidwalsh.name/concurrent-generators

【译】ES6 Generators(3)异步篇

  1. 1. 最简单的异步
  2. 2. 更好的异步
  3. 3. 使用其他的 Promise 类库
  4. 4. ES7 async
  5. 5. 总结

译注1:此文带着自己的理解,不完全按原文翻译。原文地址

译注2:原文晦涩难懂的地方,尽力做了注释或修饰,方便大家理解。错误之处欢迎各位校验指正。

生成器提供了同步方式编写的代码风格,这就允许我们隐藏异步的实现细节。

我们就可以用一种非常自然的方式来表达程序的执行流程,避免了同时处理异步代码的语法和陷阱。

换句话说,我们利用生成器从内到外、从外到内双向传值的特点,将不同的值的处理交给了不同的生成器逻辑,只需要关心获取到特定的值进行某种操作,而无需关心特定的值如何产生(通过netx() 将值的产生逻辑委托出去)。

这么一来,异步处理的优点以及易读的代码结合到一起,就加强了我们程序的可维护性。

最简单的异步

举个栗子,假定我们已经有了以下代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
function makeAjaxCall(url,cb) {
// 执行一个 ajax 请求
// 请求完成后执行 `cb(result)`
}
makeAjaxCall( "http://some.url.1", function(result1){
var data = JSON.parse( result1 );
makeAjaxCall( "http://some.url.2/?id=" + data.id, function(result2){
var resp = JSON.parse( result2 );
console.log( "我们请求到的数据是: " + resp.value );
});
} );

使用简单的生成器来表达的话,就像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function request(url) {
// 调用这个普通函数来隐藏异步处理的细节
// 使用 `it.next()` 来恢复调用这个普通函数的生成器函数的迭代器
makeAjaxCall( url, function(response){
// 异步获取到数据后,给生成器发送 `response` 信号
it.next( response );
} );
// 注意: 这里没有返回值
}
function *main() {
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
var it = main();
it.next(); // 开始迭代

request() 这个工具函数只是将我们的异步请求数据的代码进行了封装,需要注意的是在回调函数中调用了生成器的 next() 方法。

当我们使用 var it = main(); 创建了一个迭代器后,紧接着使用 it.next(); 开始迭代,这时候遇到第一个 yield 中断了生成器,转而执行 request( "http://some.url.1" )

request( "http://some.url.1" ) 异步获取到数据后,在回调函数中调用 it.next(response)response 传回给生成器刚刚中断的地方,生成器将继续迭代。

这里的亮点就是,我们在生成器中无需关心异步请求的数据如何获取,我们只知道调用了 request() 后,当需要的数据获取到了,就会通知生成器继续迭代。

这么一来在生成器中我们使用同步方式的编写风格,其实我们获取到了异步数据!

同理,当我们继续调用 it.next() 时,会遇到第二个 yield 中断迭代,发出第二个请求 yield request( "http://some.url.2?id=" + data.id ) 异步获取到数据后再恢复迭代,我们依旧不用关心异步获取数据的细节了,多爽!

以上这段代码中,request() 请求的是异步 AJAX 请求,但如果我们后续改变程序给 AJAX 设置了缓存了,获取数据会先从缓存中获取,这时候没有执行真正的 AJAX 请求就不能在回调函数中调用 it.next(response) 来恢复生成器的中断了啊!

没关系,我们可以使用一个小技巧来解决这个问题,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 给 AJAX 设置缓存
var cache = {};
function request(url) {
// 请求已被缓存
if (cache[url]) {
// 使用 setTimeout 来模拟异步操作
setTimeout( function(){
it.next( cache[url] );
}, 0 );
}
// 请求未被缓存,发出真正的请求
else {
makeAjaxCall( url, function(resp){
cache[url] = resp;
it.next( resp );
} );
}
}

看,当我们给我们的程序添加了 AJAX 缓存机制甚至其他异步操作的优化时,我们只改变了 request() 这个工具函数的逻辑,而无需改动调用这个工具函数获取数据的生成器:

1
2
3
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
..

在生成器中,我们还是像以前一样调用 request() 就能获取到需要的异步数据,无需关心获取数据的细节实现!

这就是将异步操作当做一个细节实现抽象出来后展现出的魔力了!

更好的异步

上面介绍的异步方案对于简单的异步生成器来说工作良好,但用途有限,我们需要一个更强大的异步方案:使用 Promises.

如果你对 ES6 Promises 有迷惑的话,我建议你先读 我写的介绍 Promises 的文章

我们的代码目前有个严重的问题:回调多了会产生多重嵌套(即回调地狱)。

此外,我们目前还缺乏的东西有:

  1. 清晰的错误处理逻辑。我们使用 AJAX 的回调可能会检测到一个错误,然后使用 it.throw() 将错误传回给生成器,在生成器中则使用 try..catch 来捕获错误。
    一来我们需要猜测我们可能发生错误且手动添加对应的错误处理函数,二来我们的错误处理代码没法重复使用。

  2. 如果 makeAjaxCall() 函数不受我们控制,调用了多次回调的话,也会多次触发回调中的 it.next() ,生成器就会变得非常混乱。

    处理和阻止这种问题需要大量的手动工作,也非常不方便。

  3. 有时候我们需要 『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)。而生成器中的 yield 并不支持两个或多个同时进行。

以上这些问题都可以用手动编写代码的方式来解决,但谁会想每次都重新编写类似的重复的代码呢?

我们需要一个更好的可信任、可重复使用的方案来支持我们基于生成器编写异步的代码。

怎么实现?使用 Promises !

我们将原来的代码加入 Promises 的特性:

1
2
3
4
5
6
function request(url) {
// 注意: 这里返回的是一个 promise
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} );
}

request() 函数中创建了一个 promise 实例,一旦 AJAX 请求完成,这个实例将会被 resolved

我们接着将这个实例返回,这样它就能够被 yield 了。

接下来我们需要一个工具来控制我们生成器的迭代器,接收返回的 promise 实例,然后再通过 next() 来恢复生成器的中断:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
// 执行异步的生成器
// 注意: 这是简化的版本,没有处理错误
function runGenerator(g) {
// 注意:我们使用 `g()` 自动初始化了迭代器
var it = g(), ret;
// 异步地迭代
(function iterate(val){
ret = it.next( val );
// 迭代未完成
if (!ret.done) {
// 判断是否为 promise 对象,如果没有 `then()` 方法则不是
if ("then" in ret.value) {
// 等待 promise 返回
ret.value.then( iterate );
}
// 如果不是 promise 实例,则说明直接返回了一个值
else {
// 使用 `setTimeout` 模拟异步操作
setTimeout( function(){
iterate( ret.value );
}, 0 );
}
}
})();
}

注意:我们在 runGenerator() 中先生成了一个迭代器 var it = g(),然后我们会执行这个迭代器直到它完成(done: true)。

接着我们就可以使用这个 runGenerator() 了:

1
2
3
4
5
6
7
8
runGenerator( function *main(){
var result1 = yield request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = yield request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "你请求的数据是: " + resp.value );
} );

我们通过生成不同的 promise 实例,分别对这些实例进行 yield,不同的实例等待自己的 promise 被 resolve 后再执行对应的操作。

这么一来,我们只需要同时生成不同的 promise 实例,就可以『并行地』执行不只一个任务(比如同时触发两个 AJAX 请求)了。

既然我们使用了 promises 来管理生成器中处理异步的代码,我们就解决了只有在回调中才能实现的功能,这就避免了回调嵌套了。

使用 Generotos + Promises 的优点是:

  1. 我们可以使用内建的错误处理机制。虽然这没有在上面的代码片段中展示出来,但其实很简单:

    监听 promise 中的错误,使用 it.throw() 把错误抛出,然后在生成器中使用 try..catch 进行捕获和处理即可。

  2. 我们可以使用到 Promises 提供的 control/trustability 特性。

  3. Promises 提供了大量处理多并行且复杂的任务的特性。

    举个栗子:yield Promise.all([ .. ]) 方法接收一组 promise 组成的数组作为参数,然后 yield 一个 promise 提供给生成器处理,这个 promise 会等待数组里所有 promise 完成。当我们得到 yield 后的 promise 时,说明传进去的数组中的所有 promise 都已经完成,且是按照他们被传入的顺序完成的。

首先,我们体验一下错误处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 假设1: `makeAjaxCall(..)` 第一个参数判断是否有错误产生
// 假设2: `runGenerator(..)` 能捕获并处理错误
function request(url) {
return new Promise( function(resolve,reject){
makeAjaxCall( url, function(err,text){
// 如果出错,则 reject 这个 promise
if (err) reject( err );
// 否则,resolve 这个 promise
else resolve( text );
} );
} );
}
runGenerator( function *main(){
// 捕获第一个请求的错误
try {
var result1 = yield request( "http://some.url.1" );
}
catch (err) {
console.log( "Error: " + err );
return;
}
var data = JSON.parse( result1 );
// 捕获第二个请求的错误
try {
var result2 = yield request( "http://some.url.2?id=" + data.id );
} catch (err) {
console.log( "Error: " + err );
return;
}
var resp = JSON.parse( result2 );
console.log( "你请求的数据是: " + resp.value );
} );

如果一个 promise 被 reject 或遇到其他错误的话,将使用 it.throw() (代码片段中没有展示出来)抛出一个生成器的错误,这个错误能被 try..catch 捕获。

再举个使用 Promises 管理更复杂的异步操作的栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
function request(url) {
return new Promise( function(resolve,reject){
makeAjaxCall( url, resolve );
} )
// 对 promise 返回的字符串进行后处理操作
.then( function(text){
// 是否为一个重定向链接
if (/^https?:\/\/.+/.test( text )) {
// 是的话对向新链接发送请求
return request( text );
}
// 否则,返回字符串
else {
return text;
}
} );
}
runGenerator( function *main(){
var search_terms = yield Promise.all( [
request( "http://some.url.1" ),
request( "http://some.url.2" ),
request( "http://some.url.3" )
] );
var search_results = yield request(
"http://some.url.4?search=" + search_terms.join( "+" )
);
var resp = JSON.parse( search_results );
console.log( "Search results: " + resp.value );
} );

Promise.all([ .. ]) 构造了一个 promise ,等待数组中三个 promise 的完成,这个 promise 会被 yieldrunGenerator() 生成器,然后这个生成器就可以恢复迭代。

使用其他的 Promise 类库

在上面的代码片段中,我们自己编写了 runGenerator() 函数来提供 Generators + Promises 的功能,其实我们也可以使用社区里优秀的类库,举几个栗子: QCoasynquence

接下来我会简要地介绍下 asynquence 中的 runner插件 。如果你感兴趣的话,可以阅读我写的两篇深入理解 asynquence 的博文

首先,asynquence 提供了回调函数中错误为第一参数的编写风格(error-first style),举个栗子:

1
2
3
4
5
6
function request(url) {
return ASQ( function(done){
// 传进一个以错误为第一参数的回调函数
makeAjaxCall( url, done.errfcb );
} );
}

接着,asynquence 的 runner 插件会接收一个生成器作为参数,这个生成器可以处理传入的数据处理后再传出来,而所有的的错误会自动地传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// 我们使用 `getSomeValues()` 来产生一组 promise,并链式地进行异步操作
getSomeValues()
// 现在使用一个生成器来处理接收到的数据
.runner( function*(token){
var value1 = token.messages[0];
var value2 = token.messages[1];
var value3 = token.messages[2];
// 并行地执行三个 AJAX 请求
// 注意: `ASQ().all(..)` 就像之前提过的 `Promise.all(..)`
var msgs = yield ASQ().all(
request( "http://some.url.1?v=" + value1 ),
request( "http://some.url.2?v=" + value2 ),
request( "http://some.url.3?v=" + value3 )
);
// 当三个请求都执行完毕后,进入下一步
yield (msgs[0] + msgs[1] + msgs[2]);
} )
// 现在使用前面的生成器返回的值作为参数继续发送 AJAX 请求
.seq( function(msg){
return request( "http://some.url.4?msg=" + msg );
} )
// 完成了一系列请求后,我们就获取到了想要的数据
.val( function(result){
console.log( result ); // 获取数据成功!
} )
// 如果产生错误,则抛出
.or( function(err) {
console.log( "Error: " + err );
} );

ES7 async

在 ES7 草案中有一个提议,建议采用另一种新的 async 函数类型。

使用这种函数,我们可以向外部发出 promises,然后使用 async 函数自动地将这些 promises 连接起来,当 promises 完成的时候,就会恢复 async 函数自己的中断(不需要在繁杂的迭代器中手动恢复)。

这个提议如果被采纳的话,可能会像这样:

1
2
3
4
5
6
7
8
9
10
async function main() {
var result1 = await request( "http://some.url.1" );
var data = JSON.parse( result1 );
var result2 = await request( "http://some.url.2?id=" + data.id );
var resp = JSON.parse( result2 );
console.log( "The value you asked for: " + resp.value );
}
main();

我们使用 async 声明了这种异步函数类型,然后使用 main() 直接调用这个函数,而不用像使用 runGenerator()ASQ().runner() 一样进行包装。

此外,我们没有使用 yield 关键字,而是使用了新的 await 关键字来声明等待 await 后面的 promise 的完成。

总结

一言以蔽之:Generators + Promises 的组合,强大且优雅地用同步编码风格实现了复杂的异步控制操作。

使用一些简单的工具类库,比如上面提到的 QCoasynquence 等,我们可以更方便地实现这些操作。

可以预见在不久的将来,当 ES7+ 发布的时候,我们使用 async 函数甚至可以无需使用一些类库支撑就可以实现原生的异步生成器了!

(译注:本文是第三篇文章,其实还有最后一篇是讲述并发式生成器的实现思路,涉及到 CSP 的相关概念,原文中引用了比较多的东西,读起来比较晦涩难懂,怕翻译出来与原文作者想要表达的东西相差太远,就先放一边了,感兴趣的可以直接查看原文
欢迎大牛接力)

【译】ES6 Generators(2)深入篇

  1. 1. 错误处理
  2. 2. 生成器委托
  3. 3. 总结

译注1:此文带着自己的理解,不完全按原文翻译。原文地址

译注2:原文晦涩难懂的地方,尽力做了注释或修饰,方便大家理解。错误之处欢迎各位校验指正。

如果你仍然对 ES6 Generators 不熟悉的话,建议你先阅读并运行 【译】ES6 Generators 基础篇(1) 中的代码片段,理解了生成器的基础知识后,就可以阅读这篇文章了解更多的细节啦。

错误处理

ES6 中生成器的其中一个强大的特点就是:函数内部的代码编写风格是同步的,即使外部的迭代控制过程可能是异步的。

也就是说,我们可以简单地对错误进行处理,类似我们熟悉的 try..catch 语法,举个栗子:

1
2
3
4
5
6
7
8
9
function *foo() {
try {
var x = yield 3;
console.log( "x: " + x ); // 如果出错,这里可能永远不会执行
}
catch (err) {
console.log( "Error: " + err );
}
}

即使这个生成器可能会在 yield 3 处中断,当接收到外部传入的错误时,try..catch 将会捕获到。

具体一个错误是怎样传入生成器的呢,举个栗子:

1
2
3
4
5
6
var it = foo();
var res = it.next(); // { value:3, done:false }
// 我们在这里不调用 it.next() 传值进去,而是触发一个错误
it.throw( "Oops!" ); // Error: Oops!

我们可以使用 throw() 方法产生错误传进生成器中,那么在生成器中断的地方,即 yield 3 处会产生错误,然后被 try..catch 捕获。

注意:如果我们使用 throw() 方法产生一个错误传进生成器中,但没有对应的 try..catch 对错误进行捕获的话,这个错误将会被传出去,外部如果不对错误进行捕获的话,则会抛出异常:

1
2
3
4
5
6
7
8
9
10
11
function *foo() { }
var it = foo();
// 在外部进行捕获
try {
it.throw( "Oops!" );
}
catch (err) {
console.log( "Error: " + err ); // Error: Oops!
}

当然,我们也可以进行反方向的错误捕获:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function *foo() {
var x = yield 3;
var y = x.toUpperCase(); // 若 x 不是字符串的话,将抛出TypeError 错误
yield y;
}
var it = foo();
it.next(); // { value:3, done:false }
try {
it.next( 42 ); // `42` 是数字没有 `toUpperCase()` 方法,所以会出错
}
catch (err) {
console.log( err ); // 捕获到 TypeError 错误
}

生成器委托

另一个我们想做的可能是在一个生成器中调用另一个生成器。

我并不是指在一个生成器中初始化另一个生成器,而是说我们可以将一个生成器的迭代器控制交给另一个生成器。

为了实现委托,我们需要用到 yield 关键字的另一种形式:yield *,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function *foo() {
yield 3;
yield 4;
}
function *bar() {
yield 1;
yield 2;
yield *foo(); // `yield *` 将迭代器控制委托给了 `foo()`
yield 5;
}
for (var v of bar()) {
console.log( v );
}
// 1 2 3 4 5

以上这段代码应该通俗易懂:当生成器 bar() 迭代到 yield 2 时,先将控制权交给了另一个生成器 foo()迭代完后再将控制权收回,继续进行迭代。

这里使用了 for..of 循环进行示例,正如在基础篇我们知道 for..of 循环中没有暴露出 next() 方法来传递值到生成器中,所以我们可以用手动的方式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
function *foo() {
var z = yield 3;
var w = yield 4;
console.log( "z: " + z + ", w: " + w );
}
function *bar() {
var x = yield 1;
var y = yield 2;
yield *foo(); // `yield *` 将迭代器控制委托给了 `foo()`
var v = yield 5;
console.log( "x: " + x + ", y: " + y + ", v: " + v );
}
var it = bar();
it.next(); // { value:1, done:false }
it.next( "X" ); // { value:2, done:false }
it.next( "Y" ); // { value:3, done:false }
it.next( "Z" ); // { value:4, done:false }
it.next( "W" ); // { value:5, done:false }
// z: Z, w: W
it.next( "V" ); // { value:undefined, done:true }
// x: X, y: Y, v: V

尽管我们在这里只展示了一层的委托关系,但具体场景中我们当然可以使用多层的嵌套。

一个 yield * 技巧是,我们可以从被委托的生成器(比如示例中的 foo()) 获取到返回值,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
function *foo() {
yield 2;
yield 3;
return "foo"; // 返回一个值给 `yield*` 表达式
}
function *bar() {
yield 1;
var v = yield *foo();
console.log( "v: " + v );
yield 4;
}
var it = bar();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.next(); // { value:3, done:false }
it.next(); // "v: foo" { value:4, done:false } 注意:在这里获取到了返回的值
it.next(); // { value:undefined, done:true }

yield *foo() 得到了 bar() 的控制权,完成了自己的迭代操作后,返回了一个 v: foo 值 给bar() ,然后 bar() 再继续迭代下去。

yieldyield * 表达式的一个有趣的区别是:在 yield 中,返回值在 next() 中传入的,而在 yield * 中,返回值是在 return 中传入的。

此外,我们也可以在委托的生成器中进行双向的错误绑定,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
function *foo() {
try {
yield 2;
}
catch (err) {
console.log( "foo caught: " + err );
}
yield; // 中断
// 现在抛出另一个错误
throw "Oops!";
}
function *bar() {
yield 1;
try {
yield *foo();
}
catch (err) {
console.log( "bar caught: " + err );
}
}
var it = bar();
it.next(); // { value:1, done:false }
it.next(); // { value:2, done:false }
it.throw( "Uh oh!" ); // 将会在 `foo()` 内部捕获
// foo caught: Uh oh!
it.next(); // { value:undefined, done:true } --> 这里没有错误
// bar caught: Oops!

throw( "Uh oh!" ) 在代理给 foo() 的过程中,抛了个错误进去,所以错误在 foo() 中被捕获。

同理,throw "Oops!"foo() 内部抛出的错误,将会传回给 bar() 后,被 bar() 中的 try..catch 捕获到。

总结

生成器有着同步方式的编写语法,意味着我么可以使用 try..catchyield 表达式中进行错误处理。

生成器迭代器中也有一个 throw() 方法用于在中断期间向生成器内部传入一个错误,这个错误能被生成器内部的 try..catch 捕获。

yield * 允许我们将迭代器的控制权从当前的生成器中委托给另一个生成器。好处是 yield * 扮演了在生成器间传递消息和错误的角色。

了解了这么多,还有一个很重要的问题没有解决:

怎么异步地使用生成器呢?

关键是要实现这么一个机制:在异步环境中,当迭代器的 next() 方法被调用,我们需要定位到生成器中断的地方重新启动。

别担心,请听下回分解:)

【译】ES6 Generators(1)基础篇

  1. 1. 运行直到完成 (Run-To-Completion)
  2. 2. 运行可被中止 (Run..Stop..Run)
  3. 3. 生成器的语法
  4. 4. 生成器迭代器(Generator Iterator)
  5. 5. 总结

译注1:此文带着自己的理解,不完全按原文翻译。原文地址

译注2:原文晦涩难懂的地方,尽力做了注释或修饰,方便大家理解。错误之处欢迎各位校验指正。

generator 即生成器,是 ES6 中众多特性中的一种,是一个新的函数类型。

这篇文章旨在介绍 generator 的基础知识,以及告诉你在 JS 的未来,他们为何如此重要。

运行直到完成 (Run-To-Completion)

为了理清这个新的函数类型和其他函数类型有何区别,我们首先需要了解 『run to completion』 的概念。

我们知道 JS 是单线程的,所以一旦一个函数开始执行,排在队列后边的函数就必须等待这个函数执行完毕。

举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
setTimeout(function(){
console.log("Hello World");
},1);
function foo() {
// 注意: 永远不要使用这种超长的循环,这里只是为了演示方便
for (var i=0; i<=1E10; i++) {
console.log(i);
}
}
foo();
// 0..1E10
// "Hello World"

在这段代码中,我们先执行了 foo() 然后执行 setTimeout,而 foo() 中的 for 循环将花费超长的时间才能完成。

只有等待这个漫长的循环结束后,setTimeout 中的 console.log('Hello World') 才能执行。

如果 foo() 函数能够被中断会怎样呢?

这是多线程编程语言的挑战,但我们并不需要考虑这个,因为 JS 是单线程的。

运行可被中止 (Run..Stop..Run)

使用 ES6 的生成器特性,我们有了一种新的函数类型:

允许这个函数的执行被中断一次或多次,在中断的期间我们可以去做其他操作,完成后再回来恢复这个函数的执行。

如果你了解过其他并发型或多线程的语言的话,你可能知道『协作(cooperative)』:

在一个函数执行期间,允许执行中断,在中断期间与其他代码进行协作。

ES6 生成器函数在并发行为中体现了这种『协作』的特性。

在生成器函数体中,我们可以使用一个新的 yield 关键字在内部来中断函数的执行。

需要注意的是,生成器并不能恢复自己中断的执行,我们需要一个额外的控制来恢复函数的执行。

所以,一个生成器函数能够被中断和重启。那生成器函数中断自己的执行后,怎么才知道何时恢复执行呢?

我们可以使用 yield 来对外发送中断的信号,当外部返回信号时再恢复函数的执行。

生成器的语法

我们可以这样声明一个生成器函数:

1
2
3
function *foo() {
// ...
}

注意这里的星号(*)即声明了这个函数是属于生成器类型的函数。

生成器函数大多数功能与普通函数没有区别,只有一部分新颖的语法需要学习。

先介绍一个 yield 关键字:

yield ___ 也叫做 『yield 表达式』,当我们重启生成器时,会向函数内部传值,这个值为对应的 yield ___ 表达式的计算结果。

举个栗子:

1
2
3
4
function *foo() {
var x = 1 + (yield "foo");
console.log(x);
}

在这段代码中, yield "foo" 表达式将在函数中断时,向外部发送 “foo” 这个值,且当这个生成器重启时,外部传入的值将作为这个表达式的结果:

在这里,外部传入的值将会与 1 进行相加操作,然后赋值给 x

看到双向通信的特点了么?我们在生成器内部向外发送 “foo” 然后中断函数执行,然后当生成器接收到外部传入一个值时,生成器将重启,函数将恢复执行。

如果我们只是向中止函数而不对外传值时,只使用 yield 即可:

1
2
3
4
5
6
7
8
9
// 注意: `foo(..)` 在这里并不是一个生成器
function foo(x) {
console.log("x: " + x);
}
function *bar() {
yield; // 只是中断,而不向外传值
foo( yield ); // 当外部传回一个值时,将执行 foo() 操作
}

生成器迭代器(Generator Iterator)

迭代器是一种设计模式,定义了一种特殊的行为:

我们通过 next() 来获取一组有序的值。

举个栗子:我们有个数组为 [1, 2, 3, 4, 5],第一次调用 next() 将返回 1,第二次调用 next() 将返回 2,以此类推,当数组内的值都返回完毕时,继续调用 next()将返回 null 或 false。

为了从外部控制生成器函数,我们使用生成器迭代器(generator iterator)来实现,举个栗子:

1
2
3
4
5
6
7
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
}

我们先定义了一个生成器函数 foo(),接着我们调用它一次来生成一个迭代器:

1
var it = foo();

你可能会疑问为啥我们不是使用 new 关键字即 var it = new foo() 来生成迭代器?好吧,这语法背后比较复杂已经超出了我们的讨论范围了。

接下来我们就可以使用这个迭代器了:

1
console.log( it.next() ); // { value: 1, done: false }

这里的 it.next() 返回 { value: 1, done: false },其中的 value: 1yield 1 返回的值,而 done: false 表示生成器函数还没有迭代完成。

继续调用 it.next() 进行迭代:

1
2
3
4
console.log( it.next() ); // { value:2, done:false }
console.log( it.next() ); // { value:3, done:false }
console.log( it.next() ); // { value:4, done:false }
console.log( it.next() ); // { value:5, done:false }

注意我们迭代到值为 5时,done 还是为 false,是因为这时候生成器函数并未处于完成状态,我们再调用一次看看:

1
console.log( it.next() ); // { value:undefined, done:true }

这时候我们已经执行完了所有的 yield ___ 表达式,所以 done 已经为 true

你可能会好奇的是:如果我们在一个生成器函数中使用了 return,我们在外部还能获取到 yield 的值么?

答案可以是:能

1
2
3
4
5
6
7
8
9
function *foo() {
yield 1;
return 2;
}
var it = foo();
console.log( it.next() ); // { value:1, done:false }
console.log( it.next() ); // { value:2, done:true }

让我们看看当我们使用迭代器时,生成器怎么对外传值,以及怎么接收外部传入的值:

1
2
3
4
5
6
7
8
9
10
11
12
function *foo(x) {
var y = 2 * (yield (x + 1));
var z = yield (y / 3);
return (x + y + z);
}
var it = foo( 5 );
// 注意:这里没有给 `it.next()` 传值
console.log( it.next() ); // { value:6, done:false }
console.log( it.next( 12 ) ); // { value:8, done:false }
console.log( it.next( 13 ) ); // { value:42, done:true }

我们传入参数 5 先初始化了一个迭代器。

第一个 next() 中没有传递参数进去,因为这个生成器函数中没有对应的 yield 来接收参数,所以如果我们在第一个 next() 强制传参进去的话,什么都不会发生。
第一个 yield (x+1) 将返回 value: 6 到外部,此时生成器未迭代完毕,所以同时返回 done: false

第二个 next(12) 中我们传递了参数 12 进去,则表达式 yield(x+1) 会被赋值为 12,相当于:

1
2
var x = 5;
var y = 2 * 12; // => 24

第二个 yield (y/3) 将返回 value: 8 到外部,此时生成器未迭代完毕,所以同时返回 done: false

同理,在第三个 next(13) 中我们传递了参数 13 进去,则表达式 yield(y/3) 会被赋值为 13,相当于:

1
2
3
var x = 5
var y = 24;
var z = 13;

第三个 yield并不存在,所以会 return (x + y + z) 即返回 value: 42 到外部,此时生成器已迭代完毕,所以同时返回 done: true

答案也可以是:不能!

依赖 return 从生成器中返回一个值并不好,因为当生成器遇见了 for..of 循环的时候,被返回的值将会被丢弃,举个栗子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function *foo() {
yield 1;
yield 2;
yield 3;
yield 4;
yield 5;
return 6;
}
for (var v of foo()) {
console.log( v );
}
// 1 2 3 4 5
console.log( v ); // 仍然是 `5`, 而不是 `6`

看到了吧?由 foo() 创建的迭代器会被 foo..of 循环自动捕获,且会自动进行一个接一个的迭代,直到遇到 done: true,就结束了,并没有处理 return 的值。

所以,for..of 循环会忽略被返回的 6,同时因为没有暴露出 next() 方法,for..of 循环就不能用于我们在中断生成器的期间,对生成器进行传值的场景。

总结

看了以上 ES6 Generators 的基础知识,很自然地就会想我们在什么场景下会用到这个新颖的生成器呢?

当然有很多的场景能发挥生成器的这些特性了,这篇文章只是抛砖引玉,我们将继续深入挖掘生成器的魔力!

当你在最新的 Chrome nightly 或 canary 版,或 Firefox nightly版,甚至在 v0.11+ 版本的 node (带 —harmony 开启 ES6 功能)中运行了以上这些代码片段后,我们可能会产生以下疑问:

  1. 怎么进行错误处理呢?
  2. 一个生成器怎么调用另一个生成器呢?
  3. 怎么异步地使用生成器呢?

别担心,请听下回分解:)