-
Notifications
You must be signed in to change notification settings - Fork 88
ES6 中引入了 Generator
,Generator
通过封装之后,可以作为协程来进行使用。
其中对 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
方法了。
client
的 hello
方法的返回值也是个 Promise
对象,使用 yield
之后,它的返回值就变成了实际值,也就可以直接用 console.log
进行打印了。
通过上面的例子,我们可以看出,使用协程方式,Hprose 调用就被完全同步化了。这可以大大简化异步程序的编写。
因为 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
库中的 yield
只支持 thunk 函数,生成器函数,promise 对象,以及数组和对象,但是不支持普通的基本类型的数据,比如 null
, 数字,字符串等都不支持。这对于 yield
一个类型不确定的变量来说,是很不方便的。而且这跟 await
也是不兼容的。
其次,在 yield
数组和对象时,tj/co
库会自动对数组中的元素和对象中的字段递归的遍历,将其中的所有的 Promise
元素和字段替换为实际值,这对于简单的数据来说,会方便一些。但是对于带有循环引用的数组和对象来说,会导致无法获取到结果,这是一个致命的问题。即使对于不带有循环引用结构的数组和对象来说,如果该数组和对象比较复杂,这也会消耗大量的时间。而且这跟 await
也是不兼容的。
再次,对于 thunk 函数,tj/co
库会认为回调函数第一个参数必须是表示错误,从第二个参数开始才表示返回值。而这对于回调函数只有一个返回值参数的函数,或者回调函数的第一个参数不表示错误的函数来说,tj/co
库就无法使用了。
而 hprose.co
对 yield
的支持则跟 await
完全兼容,支持对所有类型的数据进行 yield
。
当 hprose.co
对 chunk 函数进行 yield
时,如果回调函数第一个参数是 Error
类型的对象才会被当做错误处理。如果回调函数只有一个参数且不是 Error
类型的对象,则作为返回值对待。如果回调函数有两个以上的参数,如果第一个参数为 null
或 undefined
,则第一个参数被当做无错误被忽略,否则,全部回调参数都被当做返回值对待。如果被当做返回值的回调参数有多个,则这多个参数被当做数组结果对待,如果只有一个,则该参数被直接当做返回值对待。
下面我们来举例说明一下:
首先我们来看一下 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
我们可以看出,await
和 hprose.co
除了对生成器的处理不同以外,其它的都相同。对于生成器函数,await
是按原样返回的,而 hprose.co
则是按照 tj/co
的方式处理。也就是说 hprose.co
综合了 await
和 tj/co
的全部优点。使用 hprose.co
比使用 await
或 tj/co
都方便。
我们来看第二个让 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/co
和 async/await
。
我们再来看看 tj/co
和 tj/thunkify
是多么的让人抓狂,以及 hprose.co
和 hprose.thunkify
是如何优雅的解决 tj/co
和 tj/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.co
和 hprose.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);
});
然后替换不同的 co
和 thunkify
,然后执行,我们会发现,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.co
和 hprose.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);
});
这里开头对 hprose
和 tj
版本的不同 co
和 thunkify
实现的引用就省略了,请大家自行脑补。
上面这段程序,如果使用 tj
版本的 co
和 thunkify
实现,运行结果是这样的:
(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
版本的 co
和 thunkify
实现,运行结果是这样的:
3
5
3
hprose
版本的运行结果再次符合预期,而 tj
版本的运行结果再次让人失望之极。
进过上面三个回合的较量,我们发现 hprose 的协程完胜 tj
和 async/await
,而且 tj
的实现是惨败,async/await
虽然比 tj
稍微好那么一点,但是跟 hprose 所实现协程比起来,也是望尘莫及。
所以,用 tj/co
和 async/await
感觉很不爽的同学,可以试试 hprose.co
了,绝对让你爽歪歪。