Skip to content
小马哥 edited this page Nov 19, 2016 · 28 revisions

基本用法

ES6 中引入了 GeneratorGenerator 通过封装之后,可以作为协程来进行使用。

其中对 Generator 封装最为著名的当属 tj/co,但是 tj/co 跟 ES2016 的 async/await 相比的话,还存在一些比较严重的缺陷。

hprose 中也引入了对 Generator 封装的协程支持,而且比 tj/co 更加完善,后面我们会详细介绍它们之间的一些差别。

下面先让我们来看一个例子:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    yield client.useService();
    console.log(yield client.hello("World"));
});

hprose.co(也可以是 hprose.Future.co)就是一个协程封装函数。它的功能是以协程的方式来执行生成器函数。该方法允许带入参数执行。

在上面的例子中,client 是一个 Hprose 的 HTTP 客户端。Hprose 的 JavaScript 版本(包括 NodeJS, HTML5, JavaScript微信小程序专用版)的客户端为异步客户端,所以它上面的调用都是异步调用。

因为 JavaScript 的 Proxy(中文版) 具有浏览器兼容性问题,所以在客户端代理对象的生成上,使用的是动态获取服务列表的方式来实现的。

上面的 yield client.useService() 语句就是用于返回这个客户端代理对象,使用默认参数调用时,会将 client 对象本身设置为代理对象,因此这里我们没有使用返回值,如果要在其它地方使用的话,最好是保存它的返回值。

client.useService() 的返回值是一个 Promise 对象,如果不使用协程,那么在使用时,需要使用 then 方法,然后在其回调中才能使用,而使用协程,直接使用 yield 就可以获得实际的代理对象了。而且因为在这里 client 本身就是代理对象,因此当 yield 返回代理对象之后,client 对象就已经被初始化好了,因此后面就可以直接调用 client 上的 hello 方法了。

clienthello 方法的返回值也是个 Promise 对象,使用 yield 之后,它的返回值就变成了实际值,也就可以直接用 console.log 进行打印了。

通过上面的例子,我们可以看出,使用协程方式,Hprose 调用就被完全同步化了。这可以大大简化异步程序的编写。

虽然上面用 Hprose 远程调用来举例,但是 co 函数所实现的协程不是只对 Hprose 远程调用有效,而是对任何返回 promise 的对象都有效。所以,即使你不使用 Hprose 远程调用,也可以使用 co 函数和 Promise 来进行异步代码的同步化编写。

协程兼容性问题

因为 Generator 是在 ES6 中引入了,所以比较老版本的 NodeJS 是不支持的,而浏览器的支持就更少了,目前只有 Chrome 和 Firefox 支持,而 IE、Opera、Safari 都不支持,HyBird App 也不支持。

那是否意味着这个功能很鸡肋呢?并不是,因为现在有许多工具可以将 ES6 代码转换为 ES5 代码,比如 Babel。其中就包括对 Generator 的支持。所以,即使你使用了协程,仍然可以通过这些转换器转换为在各种浏览器中都可以运行的程序。

为了方便用户使用,在 hprose 中还直接集成了 regenerator-runtime.js,不需要额外引入这个文件了。

微信小程序因为缺少全局对象,仍然需要使用:

const regeneratorRuntime = require("regenerator-runtime.js");

的方式来单独引入该文件,不过该文件也已经放在微信小程序专用版中了,免去了用户单独寻找该文件的麻烦。

与 tj/co 库的区别

tj/co 有以下几个方面的问题:

首先,tj/co 库中的 yield 只支持 thunk 函数,生成器函数,promise 对象,以及数组和对象,但是不支持普通的基本类型的数据,比如 null, 数字,字符串等都不支持。这对于 yield 一个类型不确定的变量来说,是很不方便的。而且这跟 await 也是不兼容的。

其次,在 yield 数组和对象时,tj/co 库会自动对数组中的元素和对象中的字段递归的遍历,将其中的所有的 Promise 元素和字段替换为实际值,这对于简单的数据来说,会方便一些。但是对于带有循环引用的数组和对象来说,会导致无法获取到结果,这是一个致命的问题。即使对于不带有循环引用结构的数组和对象来说,如果该数组和对象比较复杂,这也会消耗大量的时间。而且这跟 await 也是不兼容的。

再次,对于 thunk 函数,tj/co 库会认为回调函数第一个参数必须是表示错误,从第二个参数开始才表示返回值。而这对于回调函数只有一个返回值参数的函数,或者回调函数的第一个参数不表示错误的函数来说,tj/co 库就无法使用了。

hprose.coyield 的支持则跟 await 完全兼容,支持对所有类型的数据进行 yield

hprose.co 对 chunk 函数进行 yield 时,如果回调函数第一个参数是 Error 类型的对象才会被当做错误处理。如果回调函数只有一个参数且不是 Error 类型的对象,则作为返回值对待。如果回调函数有两个以上的参数,如果第一个参数为 nullundefined,则第一个参数被当做无错误被忽略,否则,全部回调参数都被当做返回值对待。如果被当做返回值的回调参数有多个,则这多个参数被当做数组结果对待,如果只有一个,则该参数被直接当做返回值对待。

下面我们来举例说明一下:

yield 基本类型

首先我们来看一下 tj/co 库的例子:

var co = require('co');

co(function*() {
    try {
        console.log(yield Promise.resolve("promise"));
        console.log(yield function *() { return "generator" });
        console.log(yield new Date());
        console.log(yield 123);
        console.log(yield 3.14);
        console.log(yield "hello");
        console.log(yield true);
    }
    catch (e) {
        console.error(e);
    }
});

该程序运行结果为:

promise
generator
TypeError: You may only yield a function, promise, generator, array, or object, but the following object was passed: "Sat Nov 19 2016 14:51:09 GMT+0800 (CST)"
    at next (/usr/local/lib/node_modules/co/index.js:101:25)
    at onFulfilled (/usr/local/lib/node_modules/co/index.js:69:7)
    at process._tickCallback (internal/process/next_tick.js:103:7)
    at Module.runMain (module.js:577:11)
    at run (bootstrap_node.js:352:7)
    at startup (bootstrap_node.js:144:9)
    at bootstrap_node.js:467:3

其实除了前两个,后面的几个基本类型的数据都不能被 yield。如果我们把上面代码的第一句改为:

var co = require('hprose').co;

后面的代码都不需要修改,我们来看看运行结果:

promise
generator
2016-11-19T06:54:30.081Z
123
3.14
hello
true

也就是说,hprose.co 支持对所有类型进行 yield 操作。下面我们再来看看 async/await 是什么效果:

(async function() {
    try {
        console.log(await Promise.resolve("promise"));
        console.log(await function *() { return "generator" });
        console.log(await new Date());
        console.log(await 123);
        console.log(await 3.14);
        console.log(await "hello");
        console.log(await true);
    }
    catch (e) {
        console.error(e);
    }
})();

上面的代码基本上就是把 co(function*...) 替换成了 async function...,把 yield 替换成了 await

我们来运行上面的程序,注意,对于当前版本的 node 运行时需要加上 --harmony_async_await 参数,运行结果如下:

promise
[Function]
2016-11-19T08:16:25.316Z
123
3.14
hello
true

我们可以看出,awaithprose.co 除了对生成器的处理不同以外,其它的都相同。对于生成器函数,await 是按原样返回的,而 hprose.co 则是按照 tj/co 的方式处理。也就是说 hprose.co 综合了 awaittj/co 的全部优点。使用 hprose.co 比使用 awaittj/co 都方便。

yield 数组或对象

我们来看第二个让 tj/co 崩溃的例子:

var co = require('co');

co(function*() {
    try {
        var a = [];
        for (i = 0; i < 1000000; i++) {
            a[i] = i;
        }
        var start = Date.now();;
        yield a;
        var end = Date.now();;
        console.log(end - start);
    }
    catch (e) {
        console.error(e);
    }
});

co(function*() {
    try {
        var a = [];
        a[0] = a;
        console.log(yield a);
    }
    catch (e) {
        console.error(e);
    }
});

co(function*() {
    try {
        var o = {};
        o.self = o;
        console.log(yield o);
    }
    catch (e) {
        console.error(e);
    }
});

运行该程序,我们会看到程序会卡一会儿,然后出现下面的结果:

2530
(node:70754) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 1): RangeError: Maximum call stack size exceeded
(node:70754) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.
(node:70754) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): RangeError: Maximum call stack size exceeded

上面的 2530 是第一个 co 程序段输出的结果,也就是说这个 yield 要等待 2.5 秒才能返回结果。而后面两个 co 程序段则直接调用栈溢出了。如果在实际应用中,出现了这样的数据,使用 tj/co 你的程序就会变得很慢,或者直接崩溃了。

下面看看 hprose.co 的效果,同样只替换第一句话为:

var co = require('hprose').co;

后面的代码都不需要修改,我们来看看运行结果:

7
[ [Circular] ]
{ self: [Circular] }

第一个 co 程序段用时很短,只需要 7 ms。注意,这还是包含了后面两个程序段的时间,因为这三个协程是并发的,如果去掉后面两个程序段,你看的输出可能是 1 ms 或者 0 ms。而后面两个程序段也完美的返回了带有循环引用的数据。这才是我们期望的结果。

我们再来看看 async/await 下是什么效果,程序代码如下:

(async function() {
    try {
        var a = [];
        for (i = 0; i < 1000000; i++) {
            a[i] = i;
        }
        var start = Date.now();
        await a;
        var end = Date.now();
        console.log(end - start);
    }
    catch (e) {
        console.error(e);
    }
})();

(async function() {
    try {
        var a = [];
        a[0] = a;
        console.log(await a);
    }
    catch (e) {
        console.error(e);
    }
})();

(async function() {
    try {
        var o = {};
        o.self = o;
        console.log(await o);
    }
    catch (e) {
        console.error(e);
    }
})();

运行结果如下:

14
[ [Circular] ]
{ self: [Circular] }

我们发现 async/await 的输出结果跟 hprose.co 是一致的,但是在性能上,hprose.co 则比 async/await 还要快 1 倍。因此,第二个回合,hprose.co 仍然是完胜 tj/coasync/await

yield thunk 函数

我们再来看看 tj/cotj/thunkify 是多么的让人抓狂,以及 hprose.cohprose.thunkify 是如何优雅的解决 tj/cotj/thunkify 带来的这些让人抓狂的问题的。

首先我们来看第一个问题:

tj/thunkify 返回的 thunk 函数的执行结果是一次性的,不能像 promise 结果那样被使用多次,我们来看看下面这个例子:

var co = require("co");
var thunkify = require("thunkify");

var sum = thunkify(function(a, b, callback) {
    callback(null, a + b);
});

co(function*() {
    var result = sum(1, 2);
    console.log(yield result);
    console.log(yield sum(2, 3));
    console.log(yield result);
});

这个例子很简单,输出结果你猜是啥?

3
5
3

是上面的结果吗?恭喜你,答错了!不过,这不是你的错,而是 tj/thunkify 的错,它的结果是:

3
5

什么?最后的 console.log(yield result) 输出结果哪儿去了?不好意思,tj/thunkify 解释说是为了防止 callback 被重复执行,所以就只能这么玩了。可是真的是这样吗?

我们来看看使用 hprose.cohprose.thunkify 的执行结果吧,把开头两行换成下面三行:

var hprose = require("hprose");
var co = hprose.co;
var thunkify = hprose.thunkify;

其它代码都不用改,运行它,你会发现预期的结果出来了,就是:

3
5
3

可能你还不服气,你会说,tj/thunkify 这样做是为了防止类似被 thunkify 的函数中,回调被多次调用时,yield 的结果不正确,比如:

var sum = thunkify(function(a, b, callback) {
    callback(null, a + b);
    callback(null, a + b + a);
});

co(function*() {
    var result = sum(1, 2);
    console.log(yield result);
    console.log(yield sum(2, 3));
    console.log(yield result);
});

如果 tj/thunkify 不这样做,结果可能就会变成:

3
4
5

可是真的是这样吗?你会发现,即使改成上面的样子,hprose.thunkify 配合 hprose.co 返回的结果仍然是:

3
5
3

跟预期的一样,回调函数并没有重复执行,错误的结果并没有出现。而且当需要重复 yield 结果函数时,还能够正确得到结果。

最后我们再来看一下,tj/thunkify 这样做真的解决了问题了吗?我们把代码改成下面这样:

var sum = thunkify(function(a, b, callback) {
    console.log("call sum(" + Array.prototype.join.call(arguments) + ")");
    callback(null, a + b);
    callback(null, a + b + a);
});

co(function*() {
    var result = sum(1, 2);
    console.log(yield result);
    console.log(yield sum(2, 3));
    console.log(yield result);
});

然后替换不同的 cothunkify,然后执行,我们会发现,tj 版本的输出如下:

call sum(1,2,function (){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      })
3
call sum(2,3,function (){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      })
5
call sum(1,2,function (){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      },function (){
        if (called) return;
        called = true;
        done.apply(null, arguments);
      })

hprose 版本的输出结果如下:

call sum(1,2,function () {
                thisArg = this;
                results.resolve(arguments);
            })
3
call sum(2,3,function () {
                thisArg = this;
                results.resolve(arguments);
            })
5
3

从这里,我们可以看出,tj 版本的程序在执行第二次 yield result 时,简直错的离谱,它不但没有让我们得到预期的结果,反而还重复执行了 thunkify 后的函数,而且带入的参数也完全不对了,所以,这是一个完全错误的实现。

而从 hprose 版本的输出来看,hprose 不但完美的避免了回调被重复执行,而且保证了被 thunkify 后的函数执行的结果被多次 yield 时,也不会被重复执行,而且还能够得到预期的结果,可以实现跟返回 promise 对象一样的效果。

tj 因为没有解决他所实现的 thunkify 函数带来的这些问题,所以在后期推荐大家放弃 thunkify,转而投奔到返回 promise 对象的怀抱中,而实际上,这个问题并非是不能解决的。

hprose 在对 thunkify 函数的处理上,再次完胜 tj。而这个回合中,async/await 就不用提了,因为 async/await 完全不支持对 thunk 函数进行 await

这还不是 hprose.cohprose.thunkify 的全部呢,再继续看下面这个例子:

var sum = thunkify(function(a, b, callback) {
    callback(a + b);
});

co(function*() {
    var result = sum(1, 2);
    console.log(yield result);
    console.log(yield sum(2, 3));
    console.log(yield result);
});

这里开头对 hprosetj 版本的不同 cothunkify 实现的引用就省略了,请大家自行脑补。

上面这段程序,如果使用 tj 版本的 cothunkify 实现,运行结果是这样的:

(node:75927) UnhandledPromiseRejectionWarning: Unhandled promise rejection (rejection id: 2): 3
(node:75927) DeprecationWarning: Unhandled promise rejections are deprecated. In the future, promise rejections that are not handled will terminate the Node.js process with a non-zero exit code.

而如果使用 hprose 版本的 cothunkify 实现,运行结果是这样的:

3
5
3

hprose 版本的运行结果再次符合预期,而 tj 版本的运行结果再次让人失望之极。

进过上面三个回合的较量,我们发现 hprose 的协程完胜 tjasync/await,而且 tj 的实现是惨败,async/await 虽然比 tj 稍微好那么一点,但是跟 hprose 所实现协程比起来,也是望尘莫及。

所以,用 tj/coasync/await 感觉很不爽的同学,可以试试 hprose.co 了,绝对让你爽歪歪。

多协程并发

协程内的并发

如果在同一个协程内进行远程调用,如果不加 yield 关键字,多个远程调用就是并发执行的。加上 yield 关键字,就会变成顺序执行。对于其它的异步函数也是如此。例如:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    yield client.useService();
    console.log(yield client.hello("Hprose"));
    var a = client.sum(1, 2, 3);
    var b = client.sum(4, 5, 6);
    var c = client.sum(7, 8, 9);
    console.log(yield client.sum(a, b, c));
    console.log(yield client.hello("World"));
});

在上面的例子中,client 是一个 Hprose 的异步 Http 客户端。

所以 client.hello 和 client.sum 两个调用的返回值实际上是一个 promise 对象。而 yield 关键字在这里的作用就是,可以等待调用完成并返回 promise 所包含的值,如果 promise 的最后的状态为 REJECTED,那么 yield 将抛出一个异常,异常的值为 promise 对象中的 reason 属性值。

在上面的调用中,a, b, c 三个变量都是 promise 对象,而 client.sum 可以直接接受 promise 参数作为调用参数,当 a, b, c 三个 promise 对象的状态都变为 FULFILLED 状态时,client.sum(a, b, c) 才会真正的开始调用。而获取 a,b,c 的三个调用是异步并发执行的。

上面程序的执行结果为:

Hello Hprose
45
Hello World

从结果中,我们可以看出,三次调用的结果是顺序输出的,因为这三个输出都是用 yield 来同步获取结果的。

协程间的并发

那么当开两个或多个协程时,结果是什么样子呢?我们来看一个例子:

var hprose = require('hprose');

var client = hprose.Client.create('http://hprose.com/example/');
var proxy = client.useService();

hprose.co(function*() {
    var client = yield proxy;
    for (var i = 0; i < 5; i++) {
        console.log((yield client.hello("1-" + i)));
    }
});

hprose.co(function*() {
    var client = yield proxy;
    for (var i = 0; i < 5; i++) {
        console.log((yield client.hello("2-" + i)));
    }
});

我们运行该程序之后,可以看到如下结果:

Hello 1-0
Hello 2-0
Hello 2-1
Hello 1-1
Hello 1-2
Hello 2-2
Hello 1-3
Hello 2-3
Hello 1-4
Hello 2-4

这个运行结果并不唯一,我们有可能看到不同顺序的输出,但是有一点可以保证,就是 Hello-1-X 中的 X 是按照顺序输出的,而 Hello-2-Y 中的 Y 也是按照顺序输出的。

也就是说,每个协程内的语句是按照顺序执行的,而两个协程确是并行执行的。

不过有一点要注意,上面的例子跟第一个例子有一点不同,那就是我们把 client 客户端的创建拿到了协程外面。

但是对于 client.useService 返回的 proxy,我们在两个协程中都对它进行了一次 yield,原因是我们如果不这样做,就不能保证后面的 client 已同步的获取到了服务列表。

协程的参数和返回值

co 函数允许传参给协程。

co 函数本身的返回值也是一个 promise 对象。

下面这个例子演示了传参和 co 函数返回值的使用:

var hprose = require('hprose');

function *hello(n, client) {
    var result = [];
    for (var i = 0; i < 5; i++) {
        result[i] = client.hello(n + "-" + i);
    }
    return Promise.all(result);
}

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    yield client.useService();
    var result = yield hprose.co(function *(client) {
        var result = [];
        for (var i = 0; i < 3; i++) {
            result[i] = hprose.co(hello, i, client);
        }
        return Promise.all(result);
    }, client);
    console.log(result);
});

该程序执行结果为:

[ [ 'Hello 0-0', 'Hello 0-1', 'Hello 0-2', 'Hello 0-3', 'Hello 0-4' ],
  [ 'Hello 1-0', 'Hello 1-1', 'Hello 1-2', 'Hello 1-3', 'Hello 1-4' ],
  [ 'Hello 2-0', 'Hello 2-1', 'Hello 2-2', 'Hello 2-3', 'Hello 2-4' ] ]

在这个程序里,所有的调用都是并发执行的,最后一次 yield 汇集最终所有结果。

Clone this wiki locally