基於事件的 JavaScript 編程:異步與同

JavaScript的優點之一是其如何處理異步代碼。異步代碼會被放入一個事件隊列,等到全部其餘代碼執行後才進行,而不會阻塞線程。然而,對於初學者來講,書寫異步代碼可能會比較困難。而在這篇文章裏,我將會消除你可能會有的任何困惑。javascript

理解異步代碼

JavaScript最基礎的異步函數是setTimeoutsetInterval。setTimeout會在必定時間後執行給定的函數。它接受一個回調函數做爲第一參數和一個毫秒時間做爲第二參數。如下是用法舉例:html

?
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負載、其餘任務等所致使的延遲是能夠預料到的。」
加壹
加壹
翻譯於 3年前

6人頂java

 

 翻譯的不錯哦!node

有趣的是,直到在同一程序段中全部其他的代碼執行結束後,超時纔會發生。因此若是設置了超時,同時執行了需長時間運行的函數,那麼在該函數執行完成以前,超時甚至都不會啓動。實際上,異步函數,如setTimeout和setInterval,被壓入了稱之爲 Event Loop的隊列。 

Event Loop是一個回調函數隊列。當異步函數執行時,回調函數會被壓入這個隊列。JavaScript引擎直到異步函數執行完成後,纔會開始處理事件循環。這意味着JavaScript代碼不是多線程的,即便表現的行爲類似。事件循環是一個先進先出(FIFO)隊列,這說明回調是按照它們被加入隊列的順序執行的。JavaScript被  node選作爲開發語言,就是由於寫這樣的代碼多麼簡單啊。 
加壹
加壹
翻譯於 3年前

2人頂jquery

 

 翻譯的不錯哦!git

Ajax

異步Javascript與XML(AJAX)永久性的改變了Javascript語言的情況。忽然間,瀏覽器再也不須要從新加載便可更新web頁面。 在不一樣的瀏覽器中實現Ajax的代碼可能漫長而且乏味;可是,幸好有jQuery(還有其餘庫)的幫助,咱們可以以很容易而且優雅的方式實現客戶端-服務器端通信。github

咱們可使用jQuery跨瀏覽器接口$.ajax很容易地檢索數據,然而卻不能呈現幕後發生了什麼。好比:web

?
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,可是其實是這樣的:ajax

?
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從遠端服務器接收響應結束時回調函數纔會觸發執行。編程

jinker
jinker
翻譯於 3年前

1人頂

 

 翻譯的不錯哦!

處理異步代碼

異步編程很容易陷入咱們常說的「回調地獄」。由於事實上幾乎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

QiNark
QiNark
翻譯於 3年前

3人頂

 

 翻譯的不錯哦!

命名函數

清除嵌套回調的一個便捷的解決方案是簡單的避免雙層以上的嵌套。傳遞一個命名函數給做爲回調參數,而不是傳遞匿名函數:

?
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..." );
     // 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. 例如:

?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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函數的回調函數被調用並執行或處理這兩個異步函數產生的結果或錯誤。

ITgo
ITgo
翻譯於 3年前

1人頂

 

 翻譯的不錯哦!

Promises模型

引自  CommonJS/A: 
promise表示一個操做獨立完成後返回的最終結果。
有不少庫都包含了promise模型,其中jQuery已經有了一個可以使用且很出色的promise API。jQuery在1.5版本引入了Deferred對象,並能夠在返回promise的函數中使用jQuery.Deferred的構造結果。而返回promise的函數則用於執行某種異步操做並解決完成後的延遲。 
?
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 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 );
     });
這容許你執行兩個異步函數後,等待它們的結果,以後再用先前兩個調用的結果來執行另一個函數。 
promise表示一個操做獨立完成後返回的最終結果。
在這段代碼裏,geocode方法執行了兩次並返回了一個promise。異步函數以後執行,並在其回調裏調用了resolve。而後,一旦兩次調用resolve完成,then將會執行,其接收了以前兩次調用geocode的返回結果。結果以後被傳入getRoute,此方法也返回一個promise。最終,當getRoute的promise解決後,doSomethingCoolWithDirections回調就執行了。 
加壹
加壹
翻譯於 3年前

2人頂

 

 翻譯的不錯哦!

事件

事件是另外一種當異步回調完成處理後的通信方式。一個對象能夠成爲發射器並派發事件,而另外的對象則監聽這些事件。這種類型的事件處理方式稱之爲  觀察者模式 。  backbone.js 庫在withBackbone.Events中就建立了這樣的功能模塊。 
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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 庫中用的便是這種方式。在中介者模式,有一個用於全部對象監聽和派發事件的中間人。在這種模式下,一個對象不與另外的對象產生直接聯繫,從而使得對象間都互相分離。
加壹
加壹
翻譯於 3年前

1人頂

 

 翻譯的不錯哦!

毫不要返回promise到一個公用的API。這不只關係到了API用戶對promises的使用,也使得重構更加困難。不過,內部用途的promises和外部接口的事件的結合,卻可讓應用更低耦合且便於測試。 

在先前的例子裏面,doSomethingCoolWithDirections回調函數在兩個geocode函數完成後執行。而後,doSomethingCoolWithDirections纔會得到從getRoute接收到的響應,再將其做爲消息發送出去。 
?
1
2
3
4
5
var doSomethingCoolWithDirections =  function ( route ) {
     postal.channel(  "ui" ).publish(  "directions.done" ,  {
         route: route
     });
};
這容許了應用的其餘部分不須要直接引用產生請求的對象,就能夠響應異步回調。而在取得命令時,極可能頁面的好多區域都須要更新。在一個典型的jQuery Ajax過程當中,當接收到的命令變化時,要順利的回調可能就得作相應的調整了。這可能會使得代碼難以維護,但經過使用消息,處理UI多個區域的更新就會簡單得多了。 
?
1
2
3
4
5
6
7
8
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();
另一些基於中介者模式傳送消息的庫有  amplifyPubSubJS, and  radio.js。 
加壹
加壹
翻譯於 3年前

1人頂

 

 翻譯的不錯哦!

結論

JavaScript 使得編寫異步代碼很容易. 使用 promises, 事件, 或者命名函數來避免「callback hell」. 爲獲取更多javascript異步編程信息,請點擊Async JavaScript: Build More Responsive Apps with Less . 更多的實例託管在github上,地址NetTutsAsyncJS,趕快Clone吧 !

相關文章
相關標籤/搜索