JavaScript異步編程

簡介javascript

JavaScript是一種單線程執行的腳本語言,爲了避免讓一段JavaScript代碼執行時間太久,阻塞UI的渲染或者是鼠標事件處理,一般會採用一種異步的編程模式。這裏就跟你們一塊兒瞭解一下JavaScript的異步編程模式。html

 

1、JavaScript的異步編程模式java

1.1 爲何要異步編程node

一 開始就說過,JavaScript是一種單線程執行的腳本語言(這多是因爲歷史緣由或爲了簡單而採起的設計)。它的單線程表如今任何一個函數都要從頭到 尾執行完畢以後,纔會執行另外一個函數,界面的更新、鼠標事件的處理、計時器(setTimeout、setInterval等)的執行也須要先排隊,後串 行執行。假若有一段JavaScript從頭至尾執行時間比較長,那麼在執行期間任何UI更新都會被阻塞,界面事件處理也會中止響應。這種狀況下就須要異 步編程模式,目的就是把代碼的運行打散或者讓IO調用(例如AJAX)在後臺運行,讓界面更新和事件處理可以及時地運行。git

下面是一個同步與異步執行的例子(在線測試連接http://jsfiddle.net/ghostoy/RPQgj/):github

01 <div id="output"></div>
02  
03 <button onclick="updateSync ()">Run Sync</button>
04  
05 <button onclick="updateAsync ()">Run Async</button>
06  
07 <script>
08  
09 function updateSync() {
10     for (var i = 0; i < 1000; i++) {
11         document.getElementById('output').innerHTML = i;
12     }
13 }
14  
15 function updateAsync() {
16     var i = 0;
17  
18     function updateLater() {
19         document.getElementById('output').innerHTML = (i++);
20         if (i < 1000) {
21             setTimeout(updateLater, 0);
22         }
23     }
24  
25     updateLater();
26 }
27 </script>



點擊"Run Sync"按鈕會調用updateSync的同步函數,邏輯很是簡單,循環體內每次更新output結點的內容爲i。若是在其餘多線程模型下的語言,你可 能會看到界面上以很是快的速度顯示從0到999後中止。可是在JavaScript中,你會感受按鈕按下去的時候卡了一下,而後看到一個最終結果999, 而沒有中間過程,這就是由於在updateSync函數運行過程當中UI更新被阻塞,只有當它結束退出後纔會更新UI。若是你讓這個函數的運行時間增長一下 (例如把上限改成1 000 000),你會看到更明顯的停頓,在停頓期間點擊另外一個按鈕是沒有任何反應的,只有結束以後纔會處理另外一個按鈕的點擊事件。ajax

另外一個按鈕"Run Async"會調用updateAsync函數,它是一個異步函數,乍一看邏輯比較複雜,函數裏先聲明瞭一個局部變量i和嵌套函數updateLater(關於內嵌函數的介紹請看JavaScript世界的一等公民-函數),而後調用了updateLater,在這個函數中先是更新output結點的內容爲i,而後經過setTimeout讓updateLater函數異步執行。這個函數的運行後,你會看到UI界面上從0到999快速地更新過程,這就是異步執行的結果。編程

可見,在JavaScript中異步編程甚至是一種必要的編程模式。windows

 

1.2 異步編程的優缺點api

異 步編程的優勢是顯而易見的,異步編程你能夠實現前面例子中一邊運行一邊更新的效果;或是利用異步IO讓UI運行更加流暢,好比經過 XMLHTTPRequest的異步接口獲取網絡數據,在獲取完成後再更新界面,在異步獲取數據的時候不會阻礙UI的更新。在衆多HTML5設備API的 設計中都充分採用了異步編程模式,例如W3C的File System APIFile APIIndexed Database APIWindows 8 APIPhoneGap API,服務端腳本Node JS API等等。

異步編程也有一些缺點,形成深度嵌套的函數調用,破壞了原有的簡單邏輯,讓代碼難以讀懂。

 

2、異步編程接口設計

 

2.1 W3C原生接口

W3C原生接口的設計常常採用回調函數和事件觸發形式,前者在調用異步函數時直接傳入回調函數做爲參數,後者在原始對象上綁定事件處理函數,異步函數出錯時通常不會拋出異常,而是經過調用錯誤回調函數或觸發錯誤事件。從語義上看,回調函數形式是爲了獲取某一個函數的運行結果,而事件觸發形式一般會用於表示某些狀態變化(加載、出錯、進度變化、收到消息等等)。我的或團隊開發小型項目時能夠參考這兩種形式的接口設計。

 

回調函數:例如W3C的File System API中,在請求虛擬文件系統實例、讀寫文件等接口中,都採用了回調函數的形式:

01 requestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
02  
03          // 異步獲取虛擬文件系統實例fs
04  
05 fs.root.getFile("already_there.txt", null, function (f) {
06  
07          // 獲取文件already_there.txt
08  
09              getAsText(f.file());
10  
11 }, function(err) {
12  
13          // 獲取文件出錯
14  
15 });
16  
17 }, function(err) {
18  
19          // 獲取虛擬文件系統失敗
20  
21 });

 

事件觸發:例如W3C的XMLHTTPRequest(AJAX)就是一種經過事件觸發這種形式實現,當AJAX請求成功或失敗時觸發onload、onerror事件:

01 var xhr = new XMLHTTPRequest();
02  
03 xhr.onload = function() {
04  
05          // 加載成功時觸發onload事件
06  
07 };
08  
09 xhr.onerror = function() {
10  
11          // 加載失敗時觸發onerror事件
12  
13 };
14  
15 xhr.open(‘GET', ‘/get-ajax', true);
16  
17 xhr.send(null);

 

2.2 第三方異步接口設計

採用回調函數形式的接口寫代碼,會帶來比較嚴重的函數嵌套問題,就像著名的LISP同樣,引入大量有爭議性的括號,讓原本是先後順序執行的代碼段形式上變成了一層套一層的結構,影響了JavaScript代碼邏輯的清晰性。解決這個問題,要讓邏輯上的前後順序執行的代碼,在形式上也是順序的,而不是嵌套的,這就須要更好的異步接口設計方案。

CommonJS是一個著名的JavaScript的開源組織,目標是設計與JS環境無關的標準接口,並提供像Ruby、Python相似的標準庫函數。在CommonJS中有三個異步編程模式相關的接口提案:Promises/APromises/BPromises/D。Promise,中文意思爲承諾,意思就是說承諾完成一個任務,在完成時告之是否執行成功,並返回結果。

這 裏咱們只介紹最簡單的異步接口Promises/A,在使用這種接口的函數時,函數的返回值是一個Promise對象,它有三種狀態:不知足條件 (unfulfilled)、知足條件(fulfilled)、失敗(failed),顧名思義不知足條件狀態就是異步函數剛剛調用,還沒有真正執行時的狀 態,知足條件就是執行成功時的狀態,失敗就是執行失敗的狀態。它的接口函數也只有一個:

then(fulfilledHandler, errorHandler, progressHandler)

這 三個參數分別是知足條件、失敗以及進度有變化時的回調函數,他們的參數分別對應異步調用的結果,而then的返回值仍然是一個Promise對象,這個對 象包含了上一步異步調用回調函數的返回值,所以能夠鏈式地寫下去,表現上成爲順序執行的邏輯。例如,假如W3C的File System API採用Promises/A的接口設計,2.1節的例子能夠寫做:

01 requestFileSystem(TEMPORARY, 1024 * 1024)
02  
03 .then(function(fs) {
04  
05          // 異步獲取虛擬文件系統實例fs
06  
07          return fs.root.getFile("already_there.txt", null);
08  
09 })
10  
11 .then(function(f) {
12  
13 // 獲取文件already_there.txt
14  
15     getAsText(f.file());
16  
17 });



看是否是清楚多了?

實現Promises/A接口的JS庫有不少,好比when.jsnode-promisepromised-io等,微軟的Windows 8 Metro應用的接口設計也採用了相同的接口設計,詳見Asynchronious Programming in JavaScript with "Promises"

 

2.3 異步同步化

第 三方的異步接口必定程度上解決了代碼邏輯與執行順序不一致的問題,可是仍然有些狀況下,讓代碼難以讀懂。咱們還以1.1節中的代碼爲 例,updateAsync即便採用Promises API並不會更好理解,而代碼實現的功能其實就是一個很簡單的循環+更新的功能。這時候就須要一些異步同步化來幫助實現。

所謂異步同步化顧名思義就是採用同步形式的語法實現異步調用。這裏簡單地介紹一下老趙的Jscex,它是一個純JavaScript實現的庫,能夠在任何瀏覽器或JavaScript環境中運行,不只支持異步同步化的編程語法,還支持並行執行等特性。用Jscex來重寫1.1節中的代碼,將是這樣(在線測試連接http://jsfiddle.net/ghostoy/ugxJJ/):

01 function updateAsync() {
02     var update = eval(Jscex.compile('async', function() {
03  
04         for (var i = 0; i < 1000; i++) {
05             document.getElementById('output').innerHTML = i;
06             $await(Jscex.Async.sleep(0));  // sleep 0 ms to make it asynchronous
07         }
08  
09     }));
10      
11     update().start();
12 }



其中update是用Jscex編譯生成的函數,它會返回一個Jscex的Task對象,經過調用它的start方法來執行這個Task。Update函 數的邏輯跟updateSync幾乎同樣,$await是Jscex增長的關鍵字,用於等待一個異步任務的調用結果,Jscex.Async.sleep 是Jscex內建的一個異步任務,用於顯式地等待幾毫秒,加入這行語句以後會被Jscex編譯器生成異步的代碼,實現一邊計算一邊更新UI的效果,代碼結 構保持簡潔清楚。

 

小結

JavaScript的異步編程模式不只是一種趨勢,並且是一種必要,所以做爲HTML5開發者是很是有必要掌握的。採用第三方的異步編程庫和異步同步化的方法,會讓代碼結構相對簡潔,便於維護,推薦開發人員掌握一二,提升團隊開發效率。

相關文章
相關標籤/搜索