這篇文章主要介紹了深入理解JavaScript編程中的同步與異步機制,不僅僅是AJAX已經深入到了各個角落,Node.js的火爆也讓JS的異步編程格外引人注目,需要的朋友可以參考下
JavaScript的優勢之一是其如何處理異步代碼。異步代碼會被放入一個事件隊列,等到所有其他代碼執行後才進行,而不會阻塞線程。然而,對於初學者來說,書寫異步代碼可能會比較困難。而在這篇文章裡,我將會消除你可能會有的任何困惑。
理解異步代碼
JavaScript最基礎的異步函數是setTimeout和setInterval。setTimeout會在一定時間後執行給定的函數。它接受一個回調函數作為第一參數和一個毫秒時間作為第二參數。以下是用法舉例:
?
1 2 3 4 5 6 7 8 9 10 11 console.log( "a" ); setTimeout(function() { console.log( "c" ) }, 500 ); setTimeout(function() { console.log( "d" ) }, 500 ); setTimeout(function() { console.log( "e" ) }, 500 ); console.log( "b" );正如預期,控制台先輸出“a”、“b”,大約500毫秒後,再看到“c”、“d”、“e”。我用“大約”是因為setTimeout事實上是不可預知的。實際上,甚至 HTML5規范都提到了這個問題:
“這個API不能保證計時會如期准確地運行。由於CPU負載、其他任務等所導致的延遲是可以預料到的。”
有趣的是,直到在同一程序段中所有其余的代碼執行結束後,超時才會發生。所以如果設置了超時,同時執行了需長時間運行的函數,那麼在該函數執行完成之前,超時甚至都不會啟動。實際上,異步函數,如setTimeout和setInterval,被壓入了稱之為Event Loop的隊列。
Event Loop是一個回調函數隊列。當異步函數執行時,回調函數會被壓入這個隊列。JavaScript引擎直到異步函數執行完成後,才會開始處理事件循環。這意味著JavaScript代碼不是多線程的,即使表現的行為相似。事件循環是一個先進先出(FIFO)隊列,這說明回調是按照它們被加入隊列的順序執行的。JavaScript被 node選做為開發語言,就是因為寫這樣的代碼多麼簡單啊。
Ajax
異步Javascript與XML(AJAX)永久性的改變了Javascript語言的狀況。突然間,浏覽器不再需要重新加載即可更新web頁面。 在不同的浏覽器中實現Ajax的代碼可能漫長並且乏味;但是,幸虧有jQuery(還有其他庫)的幫助,我們能夠以很容易並且優雅的方式實現客戶端-服務器端通訊。
我們可以使用jQuery跨浏覽器接口$.ajax很容易地檢索數據,然而卻不能呈現幕後發生了什麼。比如:
?
1 2 3 4 5 6 7 8 9 10 var data; $.ajax({ url: "some/url/1", success: function( data ) { // But, this will! console.log( data ); } }) // Oops, this won't work... console.log( data );較容易犯的錯誤,是在調用$.ajax之後馬上使用data,但是實際上是這樣的:
?
1 2 3 4 5 6 7 xmlhttp.open( "GET", "some/ur/1", true ); xmlhttp.onreadystatechange = function( data ) { if ( xmlhttp.readyState === 4 ) { console.log( data ); } }; xmlhttp.send( null );底層的XmlHttpRequest對象發起請求,設置回調函數用來處理XHR的readystatechnage事件。然後執行XHR的send方法。在XHR運行中,當其屬性readyState改變時readystatechange事件就會被觸發,只有在XHR從遠端服務器接收響應結束時回調函數才會觸發執行。
處理異步代碼
異步編程很容易陷入我們常說的“回調地獄”。因為事實上幾乎JS中的所有異步函數都用到了回調,連續執行幾個異步函數的結果就是層層嵌套的回調函數以及隨之而來的復雜代碼。
node.js中的許多函數也是異步的。因此如下的代碼基本上很常見:
?
1 2 3 4 5 6 7 8 9 10 var fs = require( "fs" ); fs.exists( "index.js", function() { fs.readFile( "index.js", "utf8", function( err, contents ) { contents = someFunction( contents ); // do something with contents fs.writeFile( "index.js", "utf8", function() { console.log( "whew! Done finally..." ); }); }); }); console.log( "executing..." );下面的客戶端代碼也很多見:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 GMaps.geocode({ address: fromAddress, callback: function( results, status ) { if ( status == "OK" ) { fromLatLng = results[0].geometry.location; GMaps.geocode({ address: toAddress, callback: function( results, status ) { if ( status == "OK" ) { toLatLng = results[0].geometry.location; map.getRoutes({ origin: [ fromLatLng.lat(), fromLatLng.lng() ], destination: [ toLatLng.lat(), toLatLng.lng() ], travelMode: "driving", unitSystem: "imperial", callback: function( e ){ console.log( "ANNNND FINALLY here's the directions..." ); // do something with e } }); } } }); } } });Nested callbacks can get really nasty, but there are several solutions to this style of coding.
嵌套的回調很容易帶來代碼中的“壞味道”,不過你可以用以下的幾種風格來嘗試解決這個問題
The problem isn't with the language itself; it's with the way programmers use the language — Async Javascript.
沒有糟糕的語言,只有糟糕的程序猿 ——異步JavaSript
命名函數
清除嵌套回調的一個便捷的解決方案是簡單的避免雙層以上的嵌套。傳遞一個命名函數給作為回調參數,而不是傳遞匿名函數:
?
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 var fromLatLng, toLatLng; var routeDone = function( e ){ console.log( "ANNNND FINALLY here's the directions..."