秒殺 tj/co 的 hprose 協程庫

ES6 中引入了 Generator,Generator 經過封裝以後,能夠做爲協程來進行使用。javascript

其中對 Generator 封裝最爲著名的當屬 tj/co,可是 tj/co 跟 ES2016 的 async/await 相比的話,還存在一些比較嚴重的缺陷。html

hprose 中也引入了對 Generator 封裝的協程支持,可是比 tj/co 更加完善,下面咱們就來詳細介紹一下它們之間的差異。html5

tj/co 有如下幾個方面的問題:java

首先,tj/co 庫中的 yield 只支持 thunk 函數,生成器函數,promise 對象,以及數組和對象,可是不支持普通的基本類型的數據,好比 null, 數字,字符串等都不支持。這對於 yield 一個類型不肯定的變量來講,是很不方便的。並且這跟 await 也是不兼容的。node

其次,在 yield 數組和對象時,tj/co 庫會自動對數組中的元素和對象中的字段遞歸的遍歷,將其中的全部的 Promise 元素和字段替換爲實際值,這對於簡單的數據來講,會方便一些。可是對於帶有循環引用的數組和對象來講,會致使沒法獲取到結果,這是一個致命的問題。即便對於不帶有循環引用結構的數組和對象來講,若是該數組和對象比較複雜,這也會消耗大量的時間。並且這跟 await 也是不兼容的。git

再次,對於 thunk 函數,tj/co 庫會認爲回調函數第一個參數必須是表示錯誤,從第二個參數開始才表示返回值。而這對於回調函數只有一個返回值參數的函數,或者回調函數的第一個參數不表示錯誤的函數來講,tj/co 庫就沒法使用了。github

hprose.coyield 的支持則跟 await 徹底兼容,支持對全部類型的數據進行 yield編程

hprose.co 對 chunk 函數進行 yield 時,若是回調函數第一個參數是 Error 類型的對象纔會被當作錯誤處理。若是回調函數只有一個參數且不是 Error 類型的對象,則做爲返回值對待。若是回調函數有兩個以上的參數,若是第一個參數爲 nullundefined,則第一個參數被當作無錯誤被忽略,不然,所有回調參數都被當作返回值對待。若是被當作返回值的回調參數有多個,則這多個參數被當作數組結果對待,若是隻有一個,則該參數被直接當作返回值對待。bootstrap

下面咱們來舉例說明一下:小程序

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 了,絕對讓你爽歪歪。

hprose 有 4 個 JavaScript 版本,它們都支持上面的協程庫,它們的地址分別是:

另外,若是你不須要使用 hprose 序列化和遠程調用的話,下面還有一個專門的從 hprose 中精簡出來的 Promise A+ 實現和協程庫:Future.js(oschina鏡像)

固然該協程庫的功能不止於此,更多介紹請參見:

相關文章
相關標籤/搜索