筆者在之前的一片部落格中簡單的討論了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
- })