探索Javascript異步編程
筆者在之前的一片博客中簡單的討論了Python和Javascript的異同,其實作為一種編程語言Javascript的異步編程是一個非常值得討論的有趣話題。
JavaScript 異步編程簡介
回調(diào)函數(shù)和異步執(zhí)行
所謂的異步指的是函數(shù)的調(diào)用并不直接返回執(zhí)行的結(jié)果,而往往是通過回調(diào)函數(shù)異步的執(zhí)行。
我們先看看回調(diào)函數(shù)是什么:
- var fn = function(callback) {
- // do something here
- ...
- callback.apply(this, para);
- };
- var mycallback = function(parameter) {
- // do someting in customer callback
- };
- // call the fn with callback as parameter
- fn(mycallback);
回調(diào)函數(shù),其實就是調(diào)用用戶提供的函數(shù),該函數(shù)往往是以參數(shù)的形式提供的。回調(diào)函數(shù)并不一定是異步執(zhí)行的。比如上述的例子中,回調(diào)函數(shù)是被同步執(zhí)行的。大部分語言都支持回調(diào),C++可用通過函數(shù)指針或者回調(diào)對象,Java一般也是使用回調(diào)對象。
在Javascript中有很多通過回調(diào)函數(shù)來執(zhí)行的異步調(diào)用,例如setTimeout()或者setInterval()。
- setTimeout(function(){
- console.log("this will be exectued after 1 second!");
- },1000);
在以上的例子中,setTimeout直接返回,匿名函數(shù)會在1000毫秒(不一定能保證是1000毫秒)后異步觸發(fā)并執(zhí)行,完成打印控制臺的操作。也就是說在異步操作的情境下,函數(shù)直接返回,把控制權(quán)交給回調(diào)函數(shù),回調(diào)函數(shù)會在以后的某一個時間片被調(diào)度執(zhí)行。那么為什么需要異步呢?為什么不能直接在當(dāng)前函數(shù)中完成操作呢?這就需要了解Javascript的線程模型了。
Javascript線程模型和事件驅(qū)動
Javascript最初是被設(shè)計成在瀏覽器中輔助提供HTML的交互功能。在瀏覽器中都包含一個Javascript引擎,Javscript程序就運行在這個引擎之中,并且只有一個線程。單線程能都帶來很多優(yōu)點,程序員們可以很開心的不用去考慮諸如資源同步,死鎖等多線程阻塞式編程所需要面對的惱人的問題。但是很多人會問,既然Javascript是單線程的,那它又如何能夠異步的執(zhí)行呢?
這就需要了解到Javascript在瀏覽器中的事件驅(qū)動(event driven)機制。事件驅(qū)動一般通過事件循環(huán)(event loop)和事件隊列(event queue)來實現(xiàn)的。假定瀏覽器中有一個專門用于事件調(diào)度的實例(該實例可以是一個線程,我們可以稱之為事件分發(fā)線程event dispatch thread),該實例的工作就是一個不結(jié)束的循環(huán),從事件隊列中取出事件,處理所有很事件關(guān)聯(lián)的回調(diào)函數(shù)(event handler)。注意回調(diào)函數(shù)是在Javascript的主線程中運行的,而非事件分發(fā)線程中,以保證事件處理不會發(fā)生阻塞。
Event Loop Code:
- while(true) {
- var event = eventQueue.pop();
- if(event && event.handler) {
- event.handler.execute(); // execute the callback in Javascript thread
- } else {
- sleep(); //sleep some time to release the CPU do other stuff
- }
- }
通過事件驅(qū)動機制,我們可以想象Javascript的編程模型就是響應(yīng)一系列的事件,執(zhí)行對應(yīng)的回調(diào)函數(shù)。很多UI框架都采用這樣的模型(例如Java Swing)。
那為什要異步呢,同步不是很好么?
異步的主要目的是處理非阻塞,在和HTML交互的過程中,會需要一些IO操作(典型的就是Ajax請求,腳本文件加載),如果這些操作是同步的,就會阻塞其它操作,用戶的體驗就是頁面失去了響應(yīng)。
綜上所述Javascript通過事件驅(qū)動機制,在單線程模型下,以異步回調(diào)函數(shù)的形式來實現(xiàn)非阻塞的IO操作。
Javascript異步編程帶來的挑戰(zhàn)
Javascript的單線程模型有很多好處,但同時也帶來了很多挑戰(zhàn)。
代碼可讀性
想象一下,如果某個操作需要經(jīng)過多個非阻塞的IO操作,每一個結(jié)果都是通過回調(diào),程序有可能會看上去像這個樣子。
- operation1(function(err, result) {
- operation2(function(err, result) {
- operation3(function(err, result) {
- operation4(function(err, result) {
- operation5(function(err, result) {
- // do something useful
- })
- })
- })
- })
- })
我們稱之為意大利面條式(spaghetti)的代碼。這樣的代碼很難維護。這樣的情況更多的會發(fā)生在server side的情況下。
流程控制
異步帶來的另一個問題是流程控制,舉個例子,我要訪問三個網(wǎng)站的內(nèi)容,當(dāng)三個網(wǎng)站的內(nèi)容都得到后,合并處理,然后發(fā)給后臺。代碼可以這樣寫:
- var urls = ['url1','url2','url3'];
- var result = [];
- for (var i = 0, len = urls.length(); i < len; i++ ) {
- $.ajax({
- url: urls[i],
- context: document.body,
- success: function(){
- //do something on success
- result.push("one of the request done successfully");
- if (result.length === urls.length()) {
- //do something when all the request is completed successfully
- }
- }});
- }
上述代碼通過檢查result的長度的方式來決定是否所有的請求都處理完成,這是一個很丑陋方法,也很不可靠。
異常和錯誤處理
通過上一個例子,我們還可以看出,為了使程序更健壯,我們還需要加入異常處理。 在異步的方式下,異常處理分布在不同的回調(diào)函數(shù)中,我們無法在調(diào)用的時候通過try...catch的方式來處理異常, 所以很難做到有效,清楚。
更好的Javascript異步編程方式
“這是***的時代,也是最糟糕的時代”
為了解決Javascript異步編程帶來的問題,很多的開發(fā)者做出了不同程度的努力,提供了很多不同的解決方案。然而面對如此眾多的方案應(yīng)該如何選擇呢?我們這就來看看都有哪些可供選擇的方案吧。
Promise
Promise 對象曾經(jīng)以多種形式存在于很多語言中。這個詞***由C++工程師用在Xanadu 項目中,Xanadu 項目是Web 應(yīng)用項目的先驅(qū)。隨后Promise 被用在E編程語言中,這又激發(fā)了Python 開發(fā)人員的靈感,將它實現(xiàn)成了Twisted 框架的Deferred 對象。
2007 年,Promise 趕上了JavaScript 大潮,那時Dojo 框架剛從Twisted框架汲取靈感,新增了一個叫做dojo.Deferred 的對象。也就在那個時候,相對成熟的Dojo 框架與初出茅廬的jQuery 框架激烈地爭奪著人氣和名望。2009 年,Kris Zyp 有感于dojo.Deferred 的影響力提出了CommonJS 之Promises/A 規(guī)范。同年,Node.js ***亮相。
在編程的概念中,future,promise,和delay表示同一個概念。Promise翻譯成中文是“承諾”,也就是說給你一個東西,我保證未來能夠做到,但現(xiàn)在什么都沒有。它用來表示異步操作返回的一個對象,該對象是用來獲取未來的執(zhí)行結(jié)果的一個代理,初始值不確定。許多語言都有對Promise的支持。
Promise的核心是它的then方法,我們可以使用這個方法從異步操作中得到返回值,或者是異常。then有兩個可選參數(shù)(有的實現(xiàn)是三個),分別處理成功和失敗的情景。
- var promise = doSomethingAync()
- promise.then(onFulfilled, onRejected)
異步調(diào)用doSomethingAync返回一個Promise對象promise,調(diào)用promise的then方法來處理成功和失敗。這看上去似乎并沒有很大的改進。仍然需要回調(diào)。但是和以前的區(qū)別在于,首先異步操作有了返回值,雖然該值只是一個對未來的承諾;其次通過使用then,程序員可以有效的控制流程異常處理,決定如何使用這個來自未來的值。
對于嵌套的異步操作,有了Promise的支持,可以寫成這樣的鏈式操作:
- operation1().then(function (result1) {
- return operation2(result1)
- }).then(function (result2) {
- return operation3(result2);
- }).then(function (result3) {
- return operation4(result3);
- }).then(function (result4) {
- return operation5(result4)
- }).then(function (result5) {
- //And so on
- });
Promise提供更便捷的流程控制,例如Promise.all()可以解決需要并發(fā)的執(zhí)行若干個異步操作,等所有操作完成后進行處理。
- var p1 = async1();
- var p2 = async2();
- var p3 = async3();
- Promise.all([p1,p2,p3]).then(function(){
- // do something when all three asychronized operation finished
- });
對于異常處理,
- doA()
- .then(doB)
- .then(null,function(error){
- // error handling here
- })
#p#
如果doA失敗,它的Promise會被拒絕,處理鏈上的下一個onRejected會被調(diào)用,在這個例子中就是匿名函數(shù)function(error){}。比起原始的回調(diào)方式,不需要在每一步都對異常進行處理。這生了不少事。
以上只是對于Promise概念的簡單陳述,Promise擁有許多不同規(guī)范建議(A,A+,B,KISS,C,D等),名字(Future,Promise,Defer),和開源實現(xiàn)。大家可以參考一下的這些鏈接。
如果你有選擇困難綜合癥,面對這么多的開源庫不知道如何決斷,先不要急,這還只是一部分,還有一些庫沒有或者不完全采用Promise的概念
Non-Promise
下面列出了其它的一些開源的庫,也可以幫助解決Javascript中異步編程所遇到的諸多問題,它們的解決方案各不相同,我這里就不一一介紹了。大家有興趣可以去看看或者試用一下。
Non-3rd Party
其實,為了解決Javascript異步編程帶來的問題,不一定非要使用Promise或者其它的開源庫,這些庫提供了很好的模式,但是你也可以通過有針對性的設(shè)計來解決。
比如,對于層層回調(diào)的模式,可以利用消息機制來改寫,假定你的系統(tǒng)中已經(jīng)實現(xiàn)了消息機制,你的code可以寫成這樣:
- eventbus.on("init", function(){
- operationA(function(err,result){
- eventbus.dispatch("ACompleted");
- });
- });
- eventbus.on("ACompleted", function(){
- operationB(function(err,result){
- eventbus.dispatch("BCompleted");
- });
- });
- eventbus.on("BCompleted", function(){
- operationC(function(err,result){
- eventbus.dispatch("CCompleted");
- });
- });
- eventbus.on("CCompleted", function(){
- // do something when all operation completed
- });
這樣我們就把嵌套的異步調(diào)用,改寫成了順序執(zhí)行的事件處理。
更多的方式,請大家參考這篇文章,它提出了解決異步的五種模式:回調(diào)、觀察者模式(事件)、消息、Promise和有限狀態(tài)機(FSM)。
下一代Javscript對異步編程的增強
ECMAScript6
下一代的Javascript標準Harmony,也就是ECMAScript6正在醞釀中,它提出了許多新的語言特性,比如箭頭函數(shù)、類(Class)、生成器(Generator)、Promise等等。其中Generator和Promise都可以被用于對異步調(diào)用的增強。
Nodejs的開發(fā)版V0.11已經(jīng)可以支持ES6的一些新的特性,使用node --harmony命令來運行對ES6的支持。
co、Thunk、Koa
koa是由Express原班人馬(主要是TJ)打造,希望提供一個更精簡健壯的nodejs框架。koa依賴ES6中的Generator等新特性,所以必須運行在相應(yīng)的Nodejs版本上。
利用Generator、co、Thunk,可以在Koa中有效的解決Javascript異步調(diào)用的各種問題。
co是一個異步流程簡化的工具,它利用Generator把一層層嵌套的調(diào)用變成同步的寫法。
- var co = require('co');
- var fs = require('fs');
- var stat = function(path) {
- return function(cb){
- fs.stat(path,cb);
- }
- };
- var readFile = function(filename) {
- return function(cb){
- fs.readFile(filename,cb);
- }
- };
- co(function *() {
- var stat = yield stat('./README.md');
- var content = yield readFile('./README.md');
- })();
通過co可以把異步的fs.readFile當(dāng)成同步一樣調(diào)用,只需要把異步函數(shù)fs.readFile用閉包的方式封裝。
利用Thunk可以進一步簡化為如下的code, 這里Thunk的作用就是用閉包封裝異步函數(shù),返回一個生成函數(shù)的函數(shù),供生成器來調(diào)用。
- var thunkify = require('thunkify');
- var co = require('co');
- var fs = require('fs');
- var stat = thunkify(fs.stat);
- var readFile = thunkify(fs.readFile);
- co(function *() {
- var stat = yield stat('./README.md');
- var content = yield readFile('./README.md');
- })();
利用co可以串行或者并行的執(zhí)行異步調(diào)用。
串行
- co(function *() {
- var a = yield request(a);
- var b = yield request(b);
- })();
并行
- co(function *() {
- var res = yield [request(a), request(b)];
- })();
總結(jié)
異步編程帶來的問題在客戶端Javascript中并不明顯,但隨著服務(wù)器端Javascript越來越廣的被使用,大量的異步IO操作使得該問題變得明顯。許多不同的方法都可以解決這個問題,本文討論了一些方法,但并不深入。大家需要根據(jù)自己的情況選擇一個適于自己的方法。
同時,隨著ES6的定義,Javascript的語法變得越來越豐富,更多的功能帶來了很多便利,然而原本簡潔,單一目的的Javascript變得復(fù)雜,也要承擔(dān)更多的任務(wù)。Javascript何去何從,讓我們拭目以待。