JavaScript的優點之一是其如何處理非同步程式碼。非同步程式碼會被放入事件佇列,等到所有其他程式碼執行後才進行,而不會阻塞執行緒。然而,對於初學者來說,書寫非同步程式碼可能會比較困難。而在這篇文章裡,我將會消除你可能會有的任何困惑。
理解非同步程式碼
JavaScript最基礎的非同步函數是setTimeout和setInterval。 setTimeout會在一定時間後執行給定的函數。它接受一個回調函數作為第一參數和一個毫秒時間作為第二參數。以下是用法舉例:
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規範都提到了這個問題:
有趣的是,直到在同一程式段中所有其餘的程式碼執行結束後,逾時才會發生。所以如果設定了逾時,同時執行了需要長時間運行的函數,那麼在該函數執行完成之前,超時甚至不會啟動。實際上,非同步函數,如setTimeout和setInterval,被壓入了稱為Event Loop的佇列。
Event Loop是回呼函數佇列。當非同步函數執行時,回呼函數會被壓入這個佇列。 JavaScript引擎直到非同步函數執行完成後,才會開始處理事件循環。這意味著JavaScript程式碼不是多執行緒的,即使表現的行為相似。事件循環是先進先出(FIFO)佇列,這說明回呼是按照它們被加入佇列的順序執行的。 JavaScript被 node選做為開發語言,就是因為寫這樣的程式碼多麼簡單。
Ajax
非同步Javascript與XML(AJAX)永久性的改變了Javascript語言的狀況。突然間,瀏覽器不再需要重新載入即可更新web頁面。 在不同的瀏覽器中實作Ajax的程式碼可能漫長且乏味;但是,幸虧有jQuery(還有其他函式庫)的幫助,我們能夠以很容易並且優雅的方式實作客戶端-伺服器端通訊。
我們可以使用jQuery跨瀏覽器介面$.ajax很容易地檢索數據,然而卻不能呈現幕後發生了什麼。如:
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,但實際上是這樣的:
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中的許多函數也是非同步的。因此如下的程式碼基本上很常見:
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..." );
下面的客戶端程式碼也很多見:
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.
嵌套的回調很容易帶來代碼中的“壞味道”,不過你可以用以下的幾種風格來嘗試解決這個問題
沒有糟糕的語言,只有糟糕的程式猿 ——非同步JavaSript
命名函數
清除巢狀回呼的一個方便的解決方案是簡單的避免雙層以上的巢狀。傳遞一個命名函數給作為回調參數,而不是傳遞匿名函數:
var fromLatLng, toLatLng; var routeDone = function( e ){ console.log( "ANNNND FINALLY here's the directions..." ); // do something with e }; var toAddressDone = 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: routeDone }); } }; var fromAddressDone = function( results, status ) { if ( status == "OK" ) { fromLatLng = results[0].geometry.location; GMaps.geocode({ address: toAddress, callback: toAddressDone }); } }; GMaps.geocode({ address: fromAddress, callback: fromAddressDone });
此外, async.js 函式庫可以幫助我們處理多重Ajax requests/responses. 例如:
async.parallel([ function( done ) { GMaps.geocode({ address: toAddress, callback: function( result ) { done( null, result ); } }); }, function( done ) { GMaps.geocode({ address: fromAddress, callback: function( result ) { done( null, result ); } }); } ], function( errors, results ) { getRoute( results[0], results[1] ); });
这段代码执行两个异步函数,每个函数都接收一个名为"done"的回调函数并在函数结束的时候调用它。当两个"done"回调函数结束后,parallel函数的回调函数被调用并执行或处理这两个异步函数产生的结果或错误。
Promises模型
引自 CommonJS/A:
有很多库都包含了promise模型,其中jQuery已经有了一个可使用且很出色的promise API。jQuery在1.5版本引入了Deferred对象,并可以在返回promise的函数中使用jQuery.Deferred的构造结果。而返回promise的函数则用于执行某种异步操作并解决完成后的延迟。
var geocode = function( address ) { var dfd = new $.Deferred(); GMaps.geocode({ address: address, callback: function( response, status ) { return dfd.resolve( response ); } }); return dfd.promise(); }; var getRoute = function( fromLatLng, toLatLng ) { var dfd = new $.Deferred(); map.getRoutes({ origin: [ fromLatLng.lat(), fromLatLng.lng() ], destination: [ toLatLng.lat(), toLatLng.lng() ], travelMode: "driving", unitSystem: "imperial", callback: function( e ) { return dfd.resolve( e ); } }); return dfd.promise(); }; var doSomethingCoolWithDirections = function( route ) { // do something with route }; $.when( geocode( fromAddress ), geocode( toAddress ) ). then(function( fromLatLng, toLatLng ) { getRoute( fromLatLng, toLatLng ).then( doSomethingCoolWithDirections ); });
这允许你执行两个异步函数后,等待它们的结果,之后再用先前两个调用的结果来执行另外一个函数。
在这段代码里,geocode方法执行了两次并返回了一个promise。异步函数之后执行,并在其回调里调用了resolve。然后,一旦两次调用resolve完成,then将会执行,其接收了之前两次调用geocode的返回结果。结果之后被传入getRoute,此方法也返回一个promise。最终,当getRoute的promise解决后,doSomethingCoolWithDirections回调就执行了。
事件
事件是另一种当异步回调完成处理后的通讯方式。一个对象可以成为发射器并派发事件,而另外的对象则监听这些事件。这种类型的事件处理方式称之为 观察者模式 。 backbone.js 库在withBackbone.Events中就创建了这样的功能模块。
var SomeModel = Backbone.Model.extend({ url: "/someurl" }); var SomeView = Backbone.View.extend({ initialize: function() { this.model.on( "reset", this.render, this ); this.model.fetch(); }, render: function( data ) { // do something with data } }); var view = new SomeView({ model: new SomeModel() });
还有其他用于发射事件的混合例子和函数库,例如 jQuery Event Emitter , EventEmitter , monologue.js ,以及node.js内建的 EventEmitter 模块。
一个类似的派发消息的方式称为 中介者模式 , postal.js 库中用的即是这种方式。在中介者模式,有一个用于所有对象监听和派发事件的中间人。在这种模式下,一个对象不与另外的对象产生直接联系,从而使得对象间都互相分离。
绝不要返回promise到一个公用的API。这不仅关系到了API用户对promises的使用,也使得重构更加困难。不过,内部用途的promises和外部接口的事件的结合,却可以让应用更低耦合且便于测试。
在先前的例子里面,doSomethingCoolWithDirections回调函数在两个geocode函数完成后执行。然后,doSomethingCoolWithDirections才会获得从getRoute接收到的响应,再将其作为消息发送出去。
var doSomethingCoolWithDirections = function( route ) { postal.channel( "ui" ).publish( "directions.done", { route: route }); };
这允许了应用的其他部分不需要直接引用产生请求的对象,就可以响应异步回调。而在取得命令时,很可能页面的好多区域都需要更新。在一个典型的jQuery Ajax过程中,当接收到的命令变化时,要顺利的回调可能就得做相应的调整了。这可能会使得代码难以维护,但通过使用消息,处理UI多个区域的更新就会简单得多了。
var UI = function() { this.channel = postal.channel( "ui" ); this.channel.subscribe( "directions.done", this.updateDirections ).withContext( this ); }; UI.prototype.updateDirections = function( data ) { // The route is available on data.route, now just update the UI }; app.ui = new UI();
另外一些基于中介者模式传送消息的库有 amplify, PubSubJS, and radio.js。
结论
JavaScript 使得编写异步代码很容易. 使用 promises, 事件, 或者命名函数来避免“callback hell”. 为获取更多javascript异步编程信息,请点击Async JavaScript: Build More Responsive Apps with Less . 更多的实例托管在github上,地址NetTutsAsyncJS,赶快Clone吧 !