天天看點

探索Javascript異步程式設計

異步程式設計帶來的問題在用戶端javascript中并不明顯,但随着伺服器端javascript越來越廣的被使用,大量的異步io操作使得該問題變得明顯。許多不同的方法都可以解決這個問題,本文讨論了一些方法,但并不深入。大家需要根據自己的情況選擇一個适于自己的方法。

探索Javascript異步程式設計

筆者在之前的一片部落格中簡單的讨論了python和javascript的異同,其實作為一種程式設計語言javascript的異步程式設計是一個非常值得讨論的有趣話題。

javascript 異步程式設計簡介

回調函數和異步執行

所謂的異步指的是函數的調用并不直接傳回執行的結果,而往往是通過回調函數異步的執行。

我們先看看回調函數是什麼:

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); 

回調函數,其實就是調用使用者提供的函數,該函數往往是以參數的形式提供的。回調函數并不一定是異步執行的。比如上述的例子中,回調函數是被同步執行的。大部分語言都支援回調,c++可用通過函數指針或者回調對象,java一般也是使用回調對象。

在javascript中有很多通過回調函數來執行的異步調用,例如settimeout()或者setinterval()。

settimeout(function(){ 

    console.log("this will be exectued after 1 second!"); 

},1000); 

以上的例子中,settimeout直接傳回,匿名函數會在1000毫秒(不一定能保證是1000毫秒)後異步觸發并執行,完成列印控制台的操作。也就是

說在異步操作的情境下,函數直接傳回,把控制權交給回調函數,回調函數會在以後的某一個時間片被排程執行。那麼為什麼需要異步呢?為什麼不能直接在目前函

數中完成操作呢?這就需要了解javascript的線程模型了。

javascript線程模型和事件驅動

javascript

最初是被設計成在浏覽器中輔助提供html的互動功能。在浏覽器中都包含一個javascript引擎,javscript程式就運作在這個引擎之中,并

且隻有一個線程。單線程能都帶來很多優點,程式員們可以很開心的不用去考慮諸如資源同步,死鎖等多線程阻塞式程式設計所需要面對的惱人的問題。但是很多人會

問,既然javascript是單線程的,那它又如何能夠異步的執行呢?

這 就需要了解到javascript在浏覽器中的事件驅動(event driven)機制。事件驅動一般通過事件循環(event

loop)和事件隊列(event

queue)來實作的。假定浏覽器中有一個專門用于事件排程的執行個體(該執行個體可以是一個線程,我們可以稱之為事件分發線程event dispatch

thread),該執行個體的工作就是一個不結束的循環,從事件隊列中取出事件,處理所有很事件關聯的回調函數(event

handler)。注意回調函數是在javascript的主線程中運作的,而非事件分發線程中,以保證事件處理不會發生阻塞。

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 

 } 

通過事件驅動機制,我們可以想象javascript的程式設計模型就是響應一系列的事件,執行對應的回調函數。很多ui架構都采用這樣的模型(例如java swing)。

那為什要異步呢,同步不是很好麼?

異步的主要目的是處理非阻塞,在和html互動的過程中,會需要一些io操作(典型的就是ajax請求,腳本檔案加載),如果這些操作是同步的,就會阻塞其它操作,使用者的體驗就是頁面失去了響應。

綜上所述javascript通過事件驅動機制,在單線程模型下,以異步回調函數的形式來實作非阻塞的io操作。

javascript異步程式設計帶來的挑戰

javascript的單線程模型有很多好處,但同時也帶來了很多挑戰。

代碼可讀性

想象一下,如果某個操作需要經過多個非阻塞的io操作,每一個結果都是通過回調,程式有可能會看上去像這個樣子。

operation1(function(err, result) { 

    operation2(function(err, result) { 

        operation3(function(err, result) { 

            operation4(function(err, result) { 

                operation5(function(err, result) { 

                    // do something useful 

                }) 

            }) 

        }) 

    }) 

}) 

我們稱之為意大利面條式(spaghetti)的代碼。這樣的代碼很難維護。這樣的情況更多的會發生在server side的情況下。

流程控制

異步帶來的另一個問題是流程控制,舉個例子,我要通路三個網站的内容,當三個網站的内容都得到後,合并處理,然後發給背景。代碼可以這樣寫:

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的長度的方式來決定是否所有的請求都處理完成,這是一個很醜陋方法,也很不可靠。

異常和錯誤處理

通過上一個例子,我們還可以看出,為了使程式更健壯,我們還需要加入異常處理。 在異步的方式下,異常處理分布在不同的回調函數中,我們無法在調用的時候通過try…catch的方式來處理異常, 是以很難做到有效,清楚。

更好的javascript異步程式設計方式

“這是最好的時代,也是最糟糕的時代”

為了解決javascript異步程式設計帶來的問題,很多的開發者做出了不同程度的努力,提供了很多不同的解決方案。然而面對如此衆多的方案應該如何選擇呢?我們這就來看看都有哪些可供選擇的方案吧。

promise

promise 對 象曾經以多種形式存在于很多語言中。這個詞最先由c++工程師用在xanadu 項目中,xanadu 項目是web

應用項目的先驅。随後promise 被用在e程式設計語言中,這又激發了python 開發人員的靈感,将它實作成了twisted

架構的deferred 對象。

2007 年,promise 趕上了javascript 大潮,那時dojo

架構剛從twisted架構汲取靈感,新增了一個叫做dojo.deferred 的對象。也就在那個時候,相對成熟的dojo

架構與初出茅廬的jquery 架構激烈地争奪着人氣和名望。2009 年,kris zyp 有感于dojo.deferred

的影響力提出了commonjs 之promises/a 規範。同年,node.js 首次亮相。

程式設計的概念中,future,promise,和delay表示同一個概念。promise翻譯成中文是“承諾”,也就是說給你一個東西,我保證未來能夠

做到,但現在什麼都沒有。它用來表示異步操作傳回的一個對象,該對象是用來擷取未來的執行結果的一個代理,初始值不确定。許多語言都有對promise的

支援。

promise的核心是它的then方法,我們可以使用這個方法從異步操作中得到傳回值,或者是異常。then有兩個可選參數(有的實作是三個),分别處理成功和失敗的情景。

var promise = dosomethingaync() 

promise.then(onfulfilled, onrejected) 

步調用dosomethingaync傳回一個promise對象promise,調用promise的then方法來處理成功和失敗。這看上去似乎并沒

有很大的改進。仍然需要回調。但是和以前的差別在于,首先異步操作有了傳回值,雖然該值隻是一個對未來的承諾;其次通過使用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()可以解決需要并發的執行若幹個異步操作,等所有操作完成後進行處理。

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 

  }) 

如果doa失敗,它的promise會被拒絕,處理鍊上的下一個onrejected會被調用,在這個例子中就是匿名函數function(error){}。比起原始的回調方式,不需要在每一步都對異常進行處理。這生了不少事。

<a target="_blank" href="http://documentup.com/kriskowal/q">q</a>

<a target="_blank" href="https://github.com/tildeio/rsvp.js">rsvp.js</a>

<a target="_blank" href="https://github.com/cujojs/when">when.js</a>

<a target="_blank" href="http://mochi.github.io/mochikit/doc/html/mochikit/async.html">mochikit.async</a>

<a target="_blank" href="https://github.com/futuresjs">futurejs</a>

<a target="_blank" href="https://github.com/kriszyp/node-promise">node-promise</a>

<a target="_blank" href="http://msdn.microsoft.com/en-us/library/windows/apps/br211867.aspx">winjs</a>

如果你有選擇困難綜合症,面對這麼多的開源庫不知道如何決斷,先不要急,這還隻是一部分,還有一些庫沒有或者不完全采用promise的概念

non-promise

下面列出了其它的一些開源的庫,也可以幫助解決javascript中異步程式設計所遇到的諸多問題,它們的解決方案各不相同,我這裡就不一一介紹了。大家有興趣可以去看看或者試用一下。

<a target="_blank" href="https://github.com/laverdet/node-fibers">node-fibers</a>

<a target="_blank" href="https://github.com/sage/streamlinejs">streamlinejs</a>

<a target="_blank" href="https://github.com/creationix/step">step</a>

<a target="_blank" href="https://github.com/willconant/flow-js">flow-js</a>

<a target="_blank" href="https://github.com/caolan/async">async</a>

<a target="_blank" href="https://github.com/fjakobs/async.js">async.js</a>

<a target="_blank" href="https://github.com/isaacs/slide-flow-control">slide-flow-control</a>

non-3rd party

其實,為了解決javascript異步程式設計帶來的問題,不一定非要使用promise或者其它的開源庫,這些庫提供了很好的模式,但是你也可以通過有針對性的設計來解決。

比如,對于層層回調的模式,可以利用消息機制來改寫,假定你的系統中已經實作了消息機制,你的code可以寫成這樣:

var co = require('co'); 

var fs = require('fs'); 

var stat = function(path) { 

  return function(cb){ 

    fs.stat(path,cb); 

  } 

}; 

var readfile = function(filename) { 

    fs.readfile(filename,cb); 

co(function *() { 

  var stat = yield stat('./readme.md'); 

  var content = yield readfile('./readme.md'); 

})(); 

這樣我們就把嵌套的異步調用,改寫成了順序執行的事件處理。

下一代javscript對異步程式設計的增強

ecmascript6

nodejs的開發版v0.11已經可以支援es6的一些新的特性,使用node –harmony指令來運作對es6的支援。

co、thunk、koa

co是一個異步流程簡化的工具,它利用generator把一層層嵌套的調用變成同步的寫法。

通過co可以把異步的fs.readfile當成同步一樣調用,隻需要把異步函數fs.readfile用閉包的方式封裝。

利用thunk可以進一步簡化為如下的code, 這裡thunk的作用就是用閉包封裝異步函數,傳回一個生成函數的函數,供生成器來調用。

利用co可以串行或者并行的執行異步調用。

var thunkify = require('thunkify'); 

var stat = thunkify(fs.stat); 

var readfile = thunkify(fs.readfile); 

串行

  var a = yield request(a); 

  var b = yield request(b); 

并行

 var res = yield [request(a), request(b)]; 

總結

步程式設計帶來的問題在用戶端javascript中并不明顯,但随着伺服器端javascript越來越廣的被使用,大量的異步io操作使得該問題變得明

顯。許多不同的方法都可以解決這個問題,本文讨論了一些方法,但并不深入。大家需要根據自己的情況選擇一個适于自己的方法。

同時,随着es6的定義,javascript的文法變得越來越豐富,更多的功能帶來了很多便利,然而原本簡潔,單一目的的javascript變得複雜,也要承擔更多的任務。javascript何去何從,讓我們拭目以待。

來源:51cto