nodejs非同步控制「co、async、Q 、『es6原生promise』、then.js、bluebird」有何優缺點?最愛哪個?哪個簡單?


要說簡單,async 是最簡單的,只是在 callback 上加了一些語法糖而已。在不是很複雜的用例下夠用了,前提是你已經習慣了 callback 風格的寫法。

then.js 上手也是比較簡單的,因為也是基於 callback 和 continuation passing,並不引入額外的概念,比起 async,鏈式 API 更流暢,個人挺喜歡的。我挺久以前寫過一個在 Node 裡面跑 shell 命令的小工具,思路差不多:https://www.npmjs.org/package/shell-task

Callback-based 方案的最大問題在於異常處理,每個 callback 都得額外接受一個異常參數,發生異常就得一個一個往後傳,異常發生後的定位很麻煩。

ES6 Promise, Q, Bluebird 核心都是 Promise,缺點嘛就是必須引入這個新概念並且要用就得所有的地方都用 Promise。對於 Node 的原生 API,需要進行二次封裝。Q 和 Bluebird 都是在實現 Promise A+ 標準的基礎上提供了一些封裝和幫助方法,比如 Promise.map 來進行並行操作等等。Promise 的一個問題就是性能,而 Bluebird 號稱速度是所有 Promise 庫里最快的。ES6 Promise 則是把 Promise 的包括進 js 標準庫里,這樣你就不需要依賴第三方實現了。

關於 Promise 能夠如何改進非同步流程,建議閱讀:http://www.html5rocks.com/en/tutorials/es6/promises/#toc-parallelism-sequencing

co 是 TJ 大神基於 ES6 generator 的非同步解決方案。要理解 co 你得先理解 ES6 generator,這裡就不贅述了。co 最大的好處就是能讓你把非同步的代碼流程用同步的方式寫出來,並且可以用 try/catch:

co(function *(){
try {
var res = yield get("http://badhost.invalid");
console.log(res);
} catch(e) {
console.log(e.code) // ENOTFOUND
}
})()

但用 co 的一個代價是 yield 後面的函數必須返回一個 Thunk 或者一個 Promise,對於現有的 API 也得進行一定程度的二次封裝。另外,由於 ES6 generator 的支持情況,並不是所以地方都能用。想用的話有兩個選擇:

1. 用支持 ES6 generator 的引擎。比如 Node 0.11+ 開啟 --harmony flag,或者直接上 iojs;

2. 用預編譯器。比如 Babel (https://babeljs.io/) , Traceur (https://github.com/google/traceur-compiler) 或是 Regenerator (https://github.com/facebook/regenerator) 把帶有 generator 的 ES6 代碼編譯成 ES5 代碼。

(延伸閱讀:基於 ES6 generator 還可以模擬 go 風格的、基於 channel 的非同步協作:Taming the Asynchronous Beast with CSP in JavaScript)

但是 generator 的本意畢竟是為了可以在循環過程中 yield 進程的控制權,用 yield 來表示 「等待非同步返回的值」 始終不太直觀。因此 ES7 中可能會包含類似 C# 的 async/await :

async function showStuff () {
var data = await loadData() // loadData 返回一個 Promise
console.log(data) // data 已經載入完畢
}

async function () {
await showStuff() // async 函數默認返回一個 Promise, 所以可以 await 另一個 async 函數
// 這裡 showStuff 已經執行完畢
}

可以看到,和用 co 寫出來的代碼很像,但語意上更清晰。因為本質上 ES7 async/await 就是基於 Promise + generator 的一套語法糖。深入閱讀:ES7 async functions

想要今天就用 ES7 async/await 也是可以的!Babel 的話可以用配套的 asyncToGenerator transform: http://babeljs.io/docs/usage/transformers/other/async-to-generator/

Traceur 和 Regenerator 對其也已經有實驗性的支持了。

另外,Fiber 其實也是一個不錯的抽象,只可惜和 ES 目前的發展不一致,不太可能應用到瀏覽器端。Fiber 和 async/await 的相似處在於程序邏輯可以用接近同步的方式表現,但應用了 Fiber 之後非同步和同步的方法調用是看不出區別的。Meteor 框架內部就大量應用了 Fiber。

---

2015 年 10 月更新:

- async/await 已經升級為 stage 3 proposal,納入正式規範指日可待

- Microsoft Edge 已經率先原生支持 async/await:JavaScript goes to Asynchronous city


如果著眼於現在和未來一段時間的話,建議用Promise,不論是ES6的還是用Q還是用bluebird,毫無疑問立即馬上開始用。

如果眼光放長一點看的話,用了co以後基本上再也不願回去了,即使是「正宮娘娘」Promise。

這co完全就是用yield/generator實現了async/await的效果啊,第一次見的時候,真是有種天馬行空的感覺(原諒我見識少)。

它不僅能夠「同步非阻塞」,也幾乎沒有剝奪我對多個非阻塞操作依賴關係或者競爭關係的精確控制,當我需要精確控制非同步流程的時候,回去用Promise甚至callback,當我需要寫的爽,一瀉千里的時候,用async/await(扯遠了)。

當然它還有一些不足,這需要實現一個類似C#里的Task庫的功能,或者是用別的思路,比如CSP,這真是一個完全打破用觀察者的方式來處理GUI事件的新思路,讓處理GUI事件變得完全用消息的產生和消費的思路了(又特么扯遠了)。

但不管怎樣,co提供了一個新的方向,直到Promise,還是「非同步」,說到底還是在用「回調」的思路來實現「非同步非阻塞」,只是async.js, Promise,它們把回調嵌套「拉平」了,我覺得它們解決的是「回調嵌套的問題」,而不是「回調的問題」。

但到了async/await,包括co模擬出來的,它都打破了「非同步」的僵局,實現了「同步非阻塞」,是的這就是大家一直都在說的非同步不一定要回調,非阻塞不一定要非同步。&co,async/await和響馬的fibjs一起往go看齊了,難怪tj去搞go去了&


簡單說:

1. Promise 是方向,將現有的回調代碼 Promisify 也是 Bluebird 的內置實現,即使 ES6 支持 Promise, bluebird 也會讓你用起來更方便。

2. Generator 配合 Promise 可以寫出下面的代碼,就像用 co, 我下面的例子是用的 bluebird 實現的。實現類似於 C# async/await 語法,出異常時保持很好的調用棧信息。

3. Async 不要和 Promise 直接混用,很容易搞暈,最好用 Promisify 過的版本,例如 async-q, async-bluebird。

```

Promise = require "bluebird"

innerFunc = (value) -&>

Promise.resolve value

xp = Promise.coroutine -&>

sum = 0

sum += yield innerFunc 0

sum += yield innerFunc 1

sum += yield innerFunc 2

xp().then (value) -&>

console.log "sum is", value # sum is 3

```

簡單來說,用 Promise不用等,現在就可以用,也應該盡量用。generator 則因為 ES6 的問題,目前還只能在非生產場景下用,但是最終和 Promise 配合也會是將來非常常見的用法。


co 真的是必用的。

雖然一開始大家其實主要拿它來 spawn 一個 generator

但自從 co v4.0 拋棄 thunk 改用 Promise 之後

不把 co 當作一個純粹的 Promise 包裝器來用就很浪費了。

Promise 作為一個底層的邏輯基礎是很好

但實際寫過的都明白哪裡不爽——

var asyncFn = async function () {
return new Promise(function (resolve, reject) {
try {
await somecall();
resolve(somevalue);
} catch (err) {
reject(err);
}
});
};

try {
console.log(await asyncFn());
} catch (err) {
console.error(err);
}

new Promise(function (resolve, reject) { 這一串實在是長得不能行……

更重要的是,即使用上了 await/async,resolve() reject() 的寫法仍然是那麼的不同步。

co 一下,情況就完全不一樣了:

var asyncFn = async function () {
return co(function * () {
await somecall(); // generator 里可以 await 嗎?我不太確定
return somevalue;
});
};

try {
console.log(await asyncFn());
} catch (err) {
console.error(err);
}

完全等價的。你看看一下省掉了多少東西。

更好的是,我不知道這算是有意為之還是個美麗的意外

co 雖然宣稱包裝 「一個 generator」 為 Promise

但實際上看一下源碼就知道,它對普通的函數也一樣處理,並將返回值包裝為 Promise:

var asyncFn2 = async function () {
return co(function () {
return somevalue;
});
};

完全可以的,它相當於返回 Promise.resolve(somevalue)。

我們再進一步。

async/await 畢竟還是 ES7 特性。而既然我們已經扯上 co 了……

你知道,co yield 跟 await 沒什麼區別的:

var asyncFn = function () {
return co(function * () {
yield somecall();
return somevalue.
});
};

var asyncFn2 = function () {
return co(function () {
return somevalue.
});
};

co(function* () {
try {
console.log(yield asyncFn());
console.log(yield asyncFn2());
} catch (err) {
console.error(err);
}
});

這樣就得到了一個統一的風格

雖然背後是由隱式的 Promise 實現的非同步調用

但它看上去完全是同步的非阻塞代碼,由普通的 try-catch 和 return 組成。

對 noder 來說,你現在就可以在 nodejs 5 下使用這個風格,沒有任何額外的預編譯需求

它比 ES7 僅多最外面用作 spawn 的一層 co(function* () { .. })

在某些框架下——比如同樣基於 co-generator 的 koa——這還是隱含的。

還可以再乾淨一點嗎?

最後的福利屬於 coffeer。

ES6 引入的 arrow function 不能使用 yield 和 generator

因此你還是得老老實實包 function () { return co(function* () { .... }); } 這麼一層外皮

——但 coffeescript 可沒這個限制。

而且提到 generator,你知道 coffee 的作者對這東西是什麼態度 -_,-

There"s no function*(){}
nonsense — a generator in CoffeeScript is simply a function that yields.

……嗯。什麼意思呢?意思是連 co(function () 和 co(function * () 的區別也沒了。

asyncFn = -&> co =&>
yield somecall()
somevalue

asyncFn2 = -&> co =&>
somevalue

co -&> try
console.log yield asyncFn()
console.log yield asyncFn2()
catch err
console.error err

以上就是我現在平時在用的代碼。

如果說這東西對我來說還有什麼問題的話,它弄得我相當缺乏追新版語法的動力啊……

附註:

這個單雙相連的 arrow function 是個小技巧。

-&> co =&>

用這種格式寫一個對象的成員方法,你會體會到它的好處的

class Claz
constructor: (@name) -&>
...
foo: -&> co =&>
return @name

附註2:

使用 co-yield 調用一個舊的 node-callback 格式的非同步函數的話,我推薦這種寫法:

fs = require "fs"
co -&>
content = yield (cb) -&> fs.readFile("/etc/blah", "utf-8", cb)
console.log content

yield 一個臨時創建的 thunk,很方便。


原生 Promise 還沒有長大, 我自己先寫了個補丁來用:如何實現一個ECMAScript 6 的promise補丁。 等長大了,就用原生的 promise。


co:不作就不會死

async:Node.js 用起來還是很爽的

Q:到底 Q 到什麼程度,到底有多接近標準?

es6原生promise:就你了,不過你還沒長大成人啊

then.js:Teambition 御用非同步庫,誰用誰知道

bluebird:答主沒感覺,聽起來怎麼像一個 css 框架?


提到了這麼多的庫,就是沒提大微軟的rx.js啊??


async最簡單,也最實用。

重構代碼,順手把async代碼全用async/await+promise重寫了一遍。

感覺還不錯,用熟了完全就是體力活。

體驗上並不比async來得簡單。

像async 很常用的waterfall的,promise就作不到,得組織obj,resolve(),或是在上層聲明變數。

結果看上去像同步了,但代碼量卻多了

要用async/await+promise 不用等es7,直接上typescript


用ES6原生的Promise吧。我這有一個JS實現,和ES6自帶的Promise的API兼容,可以用於不支持Promise的瀏覽器,已經在多個實際項目中使用了。

Niman/NPromise.js at master · felixgrey/Niman · GitHub


bluebird 好過 native

(至少在目前)

就醬


以上都不好用。用我寫的haxe-continuation才好。


回調就是在合適的時候激發而已,自己實現done,fail,progress就ok


nodejs已經可以使用ES7 async await了,用法也和C#非常了類似,只是並非基於task,而是promise的一種進階。過度更加平滑,generator 由於寫出來就不平滑了,所以推薦按ES6標準先按promise寫非同步方法,過渡到async await調用。給那些噴C# async await捧promise,generate的人打臉了,不好意思,原來它們的最終形態就是async await。

老趙的那個windjs由於不是語言級別底層實現,而且為了瀏覽器兼容基於ES5還是更低的版本,使用複雜支持度低,可以光榮退休了。

--------------------------

多年後的補充:

事實證明,聽我的沒錯的

我給大家演示一下為何async await即便只是個語法糖,也遠優於then寫法

var data1 = await getdata1();

var data2 = await getdata2();

var data3 = await getdata3(data1);

......

-----------

var data1 ;

getdata1()

.then(function(d1){

data1 = d1;

return getdata2();

}).then(function(data2){

return getdata3(data1);

}).then(function(data3){

.....

})

參數的使用很多時候並不是只用剛才返回的結果,像這樣,光promise寫起來也很噁心


co + 一個Promise實現,誰用誰知道!!!

Promise單著用的,我想說誰用誰SB!

hexo@3 就是這樣,代碼比2.x 版本用async 寫的更混亂!


只要是回調都很坑...實話


目前最佳選擇是用async/await,babel 等預編譯器可支持。


First, I chose thenjs as the first postion,but a problem encountered, I found there was no way to discuss or talk,so I had to give it up.

replaced it with original promisejs.


個人認為這個得看項目情況,比如你的項目規模,比如小項目的話,個人認為then.js不錯,但在大型項目中雖然性能好,但是上面所有人沒有提到它的一個大缺陷:異常和錯誤處理!一出現異常需要在callback中層層捕捉,否則debug非常困難,大型項目中你就要哭了。


原生的async/await應該是最有吸引力的方案了,代碼的可讀性最好。

在支持async/await之前,co的解決方案是最讓人眼前一亮的,不過代碼可讀性比原生的略差一點,但要好於原生Promise/bluebird。

async是基於回調的,如今看來是可讀性不如那些基於promise的方案。


可能是我見識少了吧, 還是覺得async好用


為什麼沒有提到 老趙的 wind.js.... @趙劼


async/await,已納入es7規範,目前babel轉先,誰用誰知道啊。


推薦閱讀:

在Unity中StartCoroutine/yield return這個模式到底是怎麼應用的?其中的原理是什麼?
Vert.x性能如何,如何評價Vert.x萬事皆非同步的特性?
怎麼看:Python 3.5 支持 async/await ?

TAG:JavaScript | Nodejs | 非同步 | 回調函數Callback | npm |