Skip to content
小马哥 edited this page Dec 5, 2016 · 3 revisions

基本用法

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

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

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

下面的例子都是在 nodejs 的运行环境中执行的,但是它们对其它执行环境同样适用(除了脚本的引入方式)。

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

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 汇集最终所有结果。

wrap 包装函数和 yield 的区别

我们在 Promise 异步编程 一章中,介绍了功能强大的 wrap 函数。通过它包装的函数可以直接将 promise 对象像普通参数一样带入函数执行。但是要注意,wrap 包装之后的函数虽然看上去像是同步的,但是实际上是异步执行的。当你有多个 wrap 包装的函数顺序执行的时候,实际上并不保证执行顺序按照书写顺序来。而 yield 则是同步的,它一定会保证 yield 语句的执行顺序。

我们来看一个例子:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    yield client.useService();
    for (var i = 0; i < 5; i++) {
        console.log(yield client.hello("1-" + i));
    }
    var console_log = hprose.wrap(console.log, console);
    for (var i = 0; i < 5; i++) {
        console_log(client.hello("2-" + i));
    }
    for (var i = 0; i < 5; i++) {
        console.log(yield client.hello("3-" + i));
    }
});

运行该程序之后,执行结果为:

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

这个结果可能每次执行都不一样。

但是,Hello 1-X 始终都是按照顺序输出的,而且始终都是在 Hello 2-YHello 3-Z 之前输出的。

Hello 2-Y 的输出则不是按照顺序输出的(虽然偶尔结果也是按照顺序输出,但这一点并不能保证),而且它甚至还会穿插在 Hello 3-Z 的输出结果中。

Hello 3-Z 本身也是按照顺序输出的,但是 Hello 2-Y 却可能穿插在它的输出中间,原因是 Hello 2-Y 先执行,并且是异步执行的,因此它并不等结果执行完,就开始执行后面的语句了,所以当它执行完时,可能已经执行过几条 Hello 3-Zyield 语句了。

将协程包装成闭包函数

wrap 函数不仅仅可以将普通函数包装成支持 promise 参数的函数。

wrap 函数还支持将协程(生成器)包装成闭包函数的功能,包装之后的函数,不仅可以将协程当做普通函数一样执行,而且还支持传递 promise 参数。例如:

var hprose = require('hprose');


var coroutine = hprose.wrap(function*(client) {
    console.log(1);
    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")));
});

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    yield client.useService();
    coroutine(client);
    coroutine(Promise.resolve(client));
});

该程序执行结果为:

1
1
Hello hprose
Hello hprose
45
45
Hello world
Hello world

我们会发现通过 wrap 函数包装的协程,不再需要使用 co 函数来执行了。

协程与异常处理

在协程内,yield 不但可以将异步的 promise 结果转换成同步结果,而且可以将 REJECTED 状态的 promise 对象转换为抛出异常。例如:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    try {
        console.log(yield client.invoke('ooxx'));
    }
    catch (e) {
        console.log(e.message);
    }
});

该程序运行结果为:

Can't find this function ooxx().

在协程内抛出的异常如果没有用 try catch 语句捕获,那么第一个抛出的异常将会中断协程的执行,并将整个协程的返回值设置为 REJECTED 状态的 promise 对象,异常本身作为 reason 的值。例如:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    console.log(yield client.invoke('oo'));
    console.log(yield client.invoke('xx'));
}).catch(function(e) {
    console.log(e.message);
});

该程序运行结果为:

Can't find this function oo().

有时候,我们并不想对异常使用 try catch 处理,而是希望异常也能跟正常返回值一样被返回,那么我们可以使用 complete 方法。例如:

var hprose = require('hprose');

hprose.co(function*() {
    var client = hprose.Client.create('http://hprose.com/example/');
    console.log(yield client.invoke('oo').complete());
    console.log(yield client.invoke('xx').complete());
});

该程序运行结果如下:

Error: Can't find this function oo().
    at /usr/local/lib/node_modules/hprose/lib/client/Client.js:437:37
    at /usr/local/lib/node_modules/hprose/lib/common/Future.js:531:25
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickCallback (internal/process/next_tick.js:98:9)
Error: Can't find this function xx().
    at /usr/local/lib/node_modules/hprose/lib/client/Client.js:437:37
    at /usr/local/lib/node_modules/hprose/lib/common/Future.js:531:25
    at _combinedTickCallback (internal/process/next_tick.js:67:7)
    at process._tickCallback (internal/process/next_tick.js:98:9)

因为 complete 是 hprose 实现的 Future 类型对象上的一个方法,因此,如果你使用的是其它的 Promise 库,你可以自行实现该方法,或者先通过 hprose.Future.resolve 方法将其它方式实现的 promise 对象转换为 hprose 的 Future 对象再调用该方法。

promisify 方法

promisify 的作用是将一个使用回调的方法转换为一个返回 promise 对象的方法。它所支持的被包装的方法跟 thunkify 所支持的是一样的,但他们返回的是不同的包装函数。例如:

var hprose = require('hprose');

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

var sum1 = hprose.promisify(sum);
var sum2 = hprose.thunkify(sum);

sum1(1, 2).then(function(result) {
    console.log(result);
});

sum2(2, 3)(function(result) {
    console.log(result);
});

hprose.co(function*() {
    console.log(yield sum1(3, 4));
    console.log(yield sum2(4, 5));
});

(async function() {
    console.log(await sum1(5, 6));
    console.log(await sum2(6, 7));
})();

执行结果为:

3
5
7
9
11
[Function]

不管是通过 thunkify 包装的函数,还是通过 promisify 包装的函数,都支持在 hprose.co 协程中进行 yield 调用。但是只有 promisify 包装的函数支持在 async/await 中使用 await 调用,而 thunkify 包装的则不支持。因此,在 promisifythunkify 之间,首选 promisify

toPromise 方法

这是 Future 对象上的一个方法,在 Promise 异步编程 一章中我们介绍过,但是它还有一个功能,我们在那一章没有提到过。因为它是跟协程相关的。

在前面的介绍中,我们知道迭代器函数作为协程执行有两种方式,一种是通过 hprose.co 来直接执行,另一种是通过 hprose.wrap 来包装成普通函数执行。但是这里还有一个问题没有解决。当然这个问题比较特殊,那就是当我们要执行的函数不知道是一个普通函数还是一个迭代器函数,而且函数的返回值还是一个函数的时候,我们就不能使用 hprose.co 来直接执行,或者使用 hprose.wrap 包装一下再执行了。因为前者会导致返回的函数结果被 thunkify,后者可能会导致重复包装,降低函数执行效率,还会带来一个副作用,就是函数想要接收的参数本来就是原始的 Promise 参数的话,在这种情况下,会变成普通参数被带入,这虽然方便,但有时候却不是我们期望的方式。

我来举一个例子:

比如有这样两个函数:

function normal(p) {
    console.log(p);
    return normal;
}

function* coroutine(p) {
    console.log(yield p);
    return coroutine;
}

现在我们希望把它们作为参数带入另一个函数中执行,假设函数叫 run

function* run(fn) {
   var p = Promise.resolve(123);
   ...
}

hprose.co(function*() {
    yield run(normal);
    yield run(coroutine);
});

这里我们先不定义 run 的函数体,但是我们希望上面的执行结果跟下面这段代码一样:

hprose.co(function*() {
    var p = Promise.resolve(123);
    console.log(normal(p));
    console.log(yield coroutine(p));
})

显然这样写是不行的:

function* run(fn) {
   var p = Promise.resolve(123);
   console.log(fn(p));
}

这样写的执行结果是:

Promise { 123 }
[Function: normal]
{}

我们得不到 coroutine 执行的结果。

而如果这样写:

function* run(fn) {
   var p = Promise.resolve(123);
   console.log(yield fn(p));
}

执行结果为:

Promise { 123 }
[Function]

也不对。

如果这样写:

function* run(fn) {
   var p = Promise.resolve(123);
   console.log(yield hprose.co(fn(p)));
}

执行结果为:

Promise { 123 }
undefined
[Function: normal]
123
[Function: coroutine]

还是不对。

改成:

function* run(fn) {
   var p = Promise.resolve(123);
   console.log(yield hprose.wrap(fn)(p));
}

结果为:

123
[Function: normal]
123
[Function: coroutine]

仍然不对。

好,最后我们来看大招:

function* run(fn) {
   var p = Promise.resolve(123);
   console.log(yield hprose.Future.toPromise(fn(p)));
}

现在结果对了:

Promise { 123 }
[Function: normal]
123
[Function: coroutine]

这个结果跟我们期望的结果是一样的。这就是 Future.toPromise 的另一个用处。