JavaScript 學習筆記之線程異步模型

  核心的javascript程序語言並無包含任何的線程機制,客戶端javascript程序也沒有任何關於線程的定義,事件驅動模式下的javascript語言並不能實現同時執行,即不能同時執行兩個及以上的事件處理程序,全部的處理過程都是同步進行的。javascript的這種線程模式在大型複雜的web應用中顯得捉襟見肘,實際工做中,咱們會竭盡全力的尋找各類異步模型來彌補這一點,直到HTML5中web worker的出現才讓javascript的多線程模型出現曙光,儘管這種技術並非真正意義上實現多線程,下面就本身在工做中的摸索分享以下:javascript

javascript的線程模型前端

  客戶端js程序是相似單線程執行的,採用同步通信,即瀏覽器在響應用戶的某個事件處理過程當中,步通信會中斷瀏覽器對其餘事件或者回調的響應直至返回處理結果,而在中斷過程當中瀏覽器會變的沒有響應,看起來像死掉了同樣,因此,js腳本編程中,應避免單個事件處理程序過於複雜耗時的計算。若是實在須要,這種處理應在頁面文檔徹底加載完成後執行,且要回饋用戶當前瀏覽器是正在活動而非掛掉;實在不行,也能夠將這些大塊的複雜耗時處理過程拆分紅小的任務在後臺延時執行,以獲取最大的用戶體驗。java

同步處理過程圖形化展現以下:web

  js程序的單線程執行模型讓程序編寫更加靈活簡單,不用考慮兩個事件處理過程共同操做某個變量產生的問題,也沒必要擔憂在文檔加載過程當中其餘線程出現的更改,更不須要顧慮多線程帶來的死鎖、互斥訪問等問題。但在實際工做中,這種單線程處理過程並不能很好的知足項目的須要,HTML5中web worker在必定程度上完善了這種缺陷,下面會詳細講到。對於其同步通信過程,咱們大多會進行異步通信設計如引用setTimeOut、setInterval等異步過程函數,或者採用ajax異步回調處理等。ajax

異步處理過程圖形化展現以下:chrome

下面是在js異步處理上的探索:編程

1. 延時函數跨域

  js中經常使用的延時函數有兩個,分別是setTimeOutsetInterval,一個是將腳本塊延時到未來的某個時間點執行,另外一個是將腳本塊每隔一段時間執行一次,這兩個函數自己就是異步處理的,看下面的例子:瀏覽器

 1 setTimeout(function() {
 2     console.log("first");
 3 }, 0);  4 setInterval(function() {
 5     if (this.__Initial__ == undefined) {
 6         console.log("second");
 7         this.__Initial__="Initialed"
 8     }
 9 }, 0); 10 //主程序的
11 console.log("main");

上面定義了一個延時處理和一個間隔處理,同時主程序也添加了一個輸出處理,而陰影部分把延時和間隔都設置爲了0,即理論上當即執行,從同步通信的角度,程序順序執行,分別輸出first、second和main,實際執行結果以下:服務器

  結果好像有點意外,程序最早輸出了main,可見這兩個函數是異步處理的,從js同步通信的角度就不難理解了,首先有一個主控的程序運行環境即輸出「main」的執行上下文,其次是兩個待處理的事件處理過程(分別爲輸出「first」和輸出「second」),主控程序維持一個事件處理列表,執行順序爲輸出「main」、「first」、「second」,上述結果就是證實。

既然這兩個函數時異步的,且從上面的結果也能交替輸出,理論上就能夠實現相似多線程處理了,改寫上面的例子(以setInterval爲例)以下:

 1 //能夠看作線程1
 2 setInterval(function() {
 3     console.log("first");
 4 }, 100);
 5 //能夠看作線程2
 6 setInterval(function() {
 7     while (1) {  8         console.log("second");  9  } 10 }, 100);
11 //主程序的(能夠看作是主線程)
12 for (var i = 0; i < 10; i++) {
13     console.log("main");
14 }

此次咱們模擬線程執行的情形,以前說到過它們的執行順序,從同步的角度,應該是先輸出10次「main」,而後從setInterval異步執行的角度,「first」和「second」交替輸出。然而陰影部分定義了一個無限循環,以模仿複雜耗時的處理過程,執行上面代碼,結果以下:

  結果再次讓人意外了有木有,「main」是順利的執行了10次,而後輸出了一次「first」,接下來是無盡的輸出「second」,「first」的輸出一直處於等待狀態,頁面隨之卡死無反應,並無像理想中交替輸出「first」和「second」,這是js中單線程處理模型形成的。可見:

1. 在簡單處理邏輯的狀況下,能夠經過setInterval模擬多線程處理,可是一旦處理邏輯複雜耗時的狀況下,setInterval並不適用,系統又迴歸到單線程執行模型上了。

2. setTimeOut和setInterval在複雜耗時的狀況下會中斷瀏覽器的全部響應,故並非真正意義上的異步。

3. js中不可以經過無間隔循環監聽某一變量來實現事件處理程序的自動觸發。

相對於簡單的處理過程,經過這兩個函數實現交替執行或者觸發式響應可行,若是涉及到複雜耗時的計算,咱們必需要想辦法拆分紅子任務或者尋求其餘的異步處理機制,如ajax等。

2. Ajax(Asynchronous Javascript and XML)的異步實現

  Ajax的好處不言而喻,作過web開發的人都知道,它的出現給web開發解決了多少問題,頁面能夠動態的進行局部刷新而不是整個頁面所有刷新,頁面加載再也不是緩慢耗時而能夠先加載框架頁面而後延時豐富文檔內容,許多前端任務能夠「並行」進行而不用過度依賴javascript提供的線性路徑,頁面在處理複雜耗時的任務時仍然可以保持及時響應及互不阻塞的人機交互,這一切的一切都要歸功於Ajax的異步處理機制。下面是一個簡單的Ajax實現:

 1 function BeginRequest() {
 2     //建立XMLHttpRequest對象
 3     var request = new XMLHttpRequest();
 4     //配置一個異步請求
 5     request.open("POST", "../textPage/httpScript.ashx");
 6     //定義請求狀態變化的處理過程
 7     request.onreadystatechange = function() {
 8         if (request.readyState === 4 && request.status === 200) {
 9             alert(request.responseText);
10         }
11     }
12     //設置請求頭
13     request.setRequestHeader("content-type", "text/plain");
14     //開啓一個請求
15     request.send(null);
16 }

看以看出,普通的Ajax請求能夠分爲如下四個步驟:

第一步:建立XMLHttpRequest(這裏以XHR2爲例)對象,這能夠由被大多數瀏覽器兼容並實現的 「 new XMLHttpRequest() 」 語句來實現,但對於IE7以前的IE瀏覽器來講,須要用下面這種方式建立:

 1 //判斷是否支持XMLHttpRequest
 2     if (window.XMLHttpRequest === undefined) {
 3         //若是不支持,則定義一個
 4         window.XMLHttpRequest = function() {
 5             try {
 6                 //用最新的版本建立
 7                 return new ActiveXObject("Msxml2.XMLHTTP.6.0");
 8             }
 9             catch (ex) {
10                 try {
11                     //用舊的版本建立
12                     return new ActiveXObject("Msxml2.XMLHTTP.3.0");
13                 } catch (e) {
14                     //不支持
15                     throw new Error("XMLHttpRequest is not supported");
16                 }
17             }
18         }
19     }

第二步:經過open設置一個請求,這個請求包含兩個或者三個參數:

  1) 請求的方法(method):經常使用的有GET、POST,前者多用於請求指定資源,後者多用於表單提交。固然還有其餘的如:DELETE、HEAD、OPTIONS、PUT,這些方法根據各瀏覽器支持程度多有不一樣。

  2) 請求的URL:向服務器請求的路徑,能夠是相對於當前文檔的相對路徑,也能夠是絕對路徑(必需要指定協議、主機和端口),跨域請求將會報錯。

  3) 請求的類型:同步(false)仍是異步(true)請求,此參數爲boolean類型,默認爲異步請求,能夠不顯示傳遞此參數。

第三步:設置請求頭,經過setRequestHeader()函數進行,一般在POST請求狀況下使用,能夠設置Content-type、字符集等,此步可選。

第四步:經過send方法發送請求,若是是GET請求,則傳遞null值,若是是POST請求,則傳遞消息內容,內容必需要符合setRequestHeader設置的content-type。

  上面步驟必須按順序一一進行,否則會拋出異常。請求發出後,你必需要同時處理兩種狀況:請求成功返回以及任何在請求執行時出現的潛在錯誤,這裏經過XHR對象的onreadystatechange事件實現,此函數對http的請求狀態(status)和響應結果的準備狀態(readystate)進行監聽,詳細以下:

1 //定義請求狀態變化的處理過程
2     request.onreadystatechange = function() {
3         if (request.readyState === 4 && request.status === 200) {
4             callback(request.responseText);
5         }
6     }

其中,http請求狀態status是一個數值,如200表示請求成功,404表示請求目標未找到。響應狀態readState也是一個數值,分別表示請求響應過程的各個階段狀態,對照表以下:

表明的常量

所處的階段

UNSENT

0

Open()尚未執行

OPENED

1

Open()執行後

HEADERS_RECEIVED

2

接收到響應頭

LOADING

3

接收到響應的主體內容

DONE

4

響應完成

 

 

 

 

 

 

 

 

 

因此,在使用時也能夠用各值的常量代替,若是XMLHttpRequest.DONE。請求過程當中,上面的狀態會經歷0~4的變化,每次變化將會觸發onreadystatechange函數,響應的結果包含三個部分:

  1) 標記請求是成功或者失敗的狀態值;

  2) 響應頭的集合;

  3) 請求響應的主體內容。

上面咱們經過onreadystatechange判斷請求狀態(status和readyState)來肯定請求成功仍是失敗,成功時,經過請求對象的responseText或者responseXML屬性獲取響應結果,而後經過回調函數callback對結果進行處理,失敗時根據請求的狀態給用戶以反饋。

  咱們看到,經過對狀態值的判斷來進行決策未免過於繁瑣和混亂,因而XHR2在其中定義了一系列響應過程事件,這些事件覆蓋到請求響應的各個階段,實現這些事件的所有或者部分而不用再對狀態進行判斷就能夠獲取咱們所想要的信息,大大簡化和清晰了代碼的流程,這些事件集合見下表:

事件名稱

事件觸發階段描述

Loadstart

Send()執行後觸發。

Process

請求響應結果返回時觸發,若是請求響應過程太快,則不會觸發此函數。

Load

請求完成,即響應結束時觸發,請求完成並不表明成功,函數中仍須要對狀態進行判斷來肯定請求成功與否。

Timeout

錯誤處理函數,請求超時時觸發。

Abort

錯誤處理函數,請求終止時觸發。

Error

錯誤處理函數,其餘網絡錯誤出現時觸發。

Loadend

全部處理過程完成後觸發。

 

 

 

 

 

 

 

 

 

 

 

 

注:上述函數集合,在Firefox、Chrome和Safari中支持良好,IE支持較差。這些函數中的Load、Timeout、Abort、Error,對於某個請求,它們之中的一個將被執行,由於結果要麼是成功,要麼是失敗,不會出現兩種狀況同時存在的。這些函數的的調用方式可經過XMLHttpRequest的對象request進行,如request.onload\request.onprocess等,也能夠經過addEventListener添加事件,代碼以下:

 1 function BeginRequest() {
 2     //建立XMLHttpRequest對象
 3     var request = new XMLHttpRequest();
 4     //配置一個異步請求
 5     request.open("GET", "../textPage/httpScript.ashx"); 
 6     
 7     //請求開始時的處理事件
 8     request.onloadstart = function() {
 9         console.log("loadstart: the send method has been just called.");
10         request.timeout = 10000;
11     };
12     //過程處理事件
13     request.onprogress = function() {
14         console.log("progress: the response is being downloaded.");
15     };
16     //響應結束時的處理事件
17     request.onload = function() {
18         console.log("load: the request is complete.");
19         if (request.readyState === 4 && request.status === 200) {
20             console.log("success: the request is success.");
21         }
22     };
23     //響應超時
24     request.ontimeout = function() {
25         console.log("timeout: the request is timeout.");
26     };
27     //請求退出
28     request.onabort = function() {
29         console.log("abort: the request is abort.");
30     }
31     //其餘錯誤處理
32     request.onerror = function() {
33         console.log("network error: unkown network-error occured.");
34     };
35     //請求全部過程結束後的處理(至關於一個finally處理)
36     request.onloadend = function() {
37         console.log("loadend: all of the progress are completed.");
38     };
39     
40     //設置請求頭
41     request.setRequestHeader("content-type", "text/plain");
42     //開啓一個請求
43     request.send(null);
44 }
45 
46 window.onload = function() {
47     BeginRequest();
48 }

上面過程將這些函數悉數定義(chrome瀏覽器),執行結果以下:

能夠看到,正常響應狀況下這些函數的執行順序,值得注意的是,load事件只是標記請求及響應過程已完成,但並不表明這個過程是成功的,故咱們在load事件裏添加了對狀態的判斷。上面經過在loadstart事件中用request.timeout=10000語句設置了該請求的超時時間爲10s,若是縮短該屬性值到10ms會有怎樣的執行結果呢:

結果在乎料之中,timeout事件執行了,load事件則沒有執行。在實際狀況中,若是請求響應過程耗時不被實際需求所容許,能夠直接經過調用abort函數取消該次請求,如請求太過頻繁,新的請求才是用戶想要的,對於舊的請求就能夠abort掉了,下面看看這個函數的效果,對於上面的例子,咱們改寫progress函數以下:

1 //過程處理事件
2     request.onprogress = function() {
3         console.log("progress: the response is being downloaded.");
4         request.abort();
5     };

執行上面的例子,結果以下:

可見,abort事件被觸發了,故XHR2能夠經過調用abort()函數來觸發一個onabort()事件。同時,progress事件的event對象含有三個有趣的屬性:loaded表示響應過程當前已經傳輸的字節數(bytes);total表示響應過程所有須要傳輸的字節數;lengthComputable表示響應過程是否知道整個傳輸內容的長度,知道則爲true,不然爲false,這樣就能夠經過loaded和total屬性實現響應過程的進度跟蹤,使用戶獲得更好的體驗。

  不難看出,XHR2的這些函數集大大提升了咱們對ajax請求狀態進行檢測和控制,如此,能夠把複雜耗時的操做扔給ajax進行異步實現,釋放應用程序去實時響應其餘消息事件,能夠大大提升用戶的體驗。從線程實現的角度,ajax確實提供了一種多線程的解決方案,但它依賴於http請求,更多時候咱們只是將複雜耗時的操做扔給了後臺執行,對於客戶端來講,咱們並無在多線程的道路上取得建設性的進展,也許HTML5中的web worker會在多線程的實現上給咱們以驚喜。

3. Html5中的Web Worker

  做爲web前端開發人員,HTML5已經給了太多的驚喜,web worker就是其中之一,它的出現對javascript的固有的單線程模型構成了巨大的挑戰,讓多線程在javascript中的定義出現了起色,它可以確實的定義多個平行的執行線程(即worker),這些workers生存於自身執行的環境空間,除了消息傳遞(如postmessage),它們不可以同建立它們的DOM或者Window進行通信,這就意味着,在javascript腳本中,咱們利用worker能夠編寫複雜耗時的處理過程以及使用同步處理API而不用擔憂整個瀏覽器卡死無響應。

  實際使用中,根據須要,能夠定義10個、100個甚至上千個worker來並行處理多個複雜耗時的邏輯,這些處理腳本都放在單獨的文件中(.js),故worker的使用分爲兩個部分,一部分在頁面DOM級主線程上(即worker對象),另外一部分在工做線程worker中(即worker處理的執行上下文),它們之間經過消息傳遞進行通信和數據交換。下面咱們經過一個例子來看看工做線程(worker)的使用:

咱們定義一個叫webworker的工做線程腳本塊,存儲在文件webworker.js中,實現以下:

 1 //引入工做線程處理過程當中依賴的js
 2 //importScripts("../aboutsort.js");
 3 
 4 //接收來自DOM級工做線程對象發送的消息
 5 onmessage = function(e) {
 6     console.log("receive the message from the main thread. the argument value is :" + e.data);
 7     //工做線程開始進行復雜耗時的邏輯處理
 8     BeginRequest();
 9 }
10 
11 function BeginRequest() {
12     //建立XMLHttpRequest對象
13     var request = new XMLHttpRequest();
14     //配置一個異步請求
15     request.open("GET", "../../textPage/httpScript.ashx");
16     //定義請求狀態變化的處理過程
17     request.onreadystatechange = function() {
18         if (request.readyState === 4 && request.status === 200) {
19             //正常處理過程被延時5秒鐘
20             setTimeout(function() {
21                 //向DOM級主線程發送消息,通常爲返回處理結果
22                 postMessage("ok.");
23 
24             }, 5000);
25         }
26         else if (request.status === 404) {
27             postMessage("fail.");
28         }
29     }
30     //設置請求頭
31     request.setRequestHeader("content-type", "text/plain");
32     //開啓一個請求
33     request.send(null);
34 }

DOM級主線程中調用的實現:

 1 (function() {
 2     //傳遞工做線程待處理的腳本塊路徑來建立工做線程對象
 3     var worker = new Worker("../js/worker/webworker.js");
 4     //向工做線程內部發送消息
 5     worker.postMessage("parameter");
 6     //接收來自工做線程的消息
 7     worker.onmessage = function(e) {
 8         console.log("the response from the worker:" + e.data);
 9         //關閉工做線程
10         worker.terminate();
11     }
12     //定義工做線程出錯時的處理過程
13     worker.onerror = function() {
14         console.log("error occured.");
15     }
16 } ());

由上能夠看出,工做線程的執行腳本是存儲在單獨的js文件中,經過調用方(DOM級主線程)引用並建立worker對象來調用,它們經過onmessage和postMessage進行消息傳遞(即請求和響應):

worker對象

1. worker對象能夠傳遞給構造函數new Worker()一個線程腳本塊的路徑URL來建立,這個URL能夠是相對路徑,也能夠是絕對路徑,絕對路徑時必需要符合javascript的同源策略。

2. 一個DOM級主線程能夠建立多個worker對象,它們可分別處理不一樣的腳本塊,這些worker對象共同依賴這個建立它們的DOM。

3. 擁有一個worker對象後,能夠經過對象的postMessage方法向腳本塊的執行上下文發送消息,消息內容可經過參數傳遞,這個參數不侷限於string,也能夠是對象,HTML5進行深度複製(structure clone)來傳遞。

4. 能夠經過worker對象的message方法接收來自worker處理線程的響應,該方法有一個參數(暫定爲e),能夠經過訪問e.data來獲取響應內容,還能夠定義對象的error方法對執行過程進行一場捕獲。

5. 消息傳遞創建後,能夠經過worker對象的terminate函數進行關閉,在關閉以前能夠有屢次消息傳遞。

6. worker對象的這些方法,能夠經過atachEvent或者addEventListener等通用事件函數實現。

worker執行上下文(WorkerGlobalScope,也即工做線程執行的腳本塊)

1. 它是worker工做線程的全局環境,與建立這個工做線程的DOM和window沒有任何關係,它是獨立的執行環境,就至關於javascript中的Global變量。

2. 在這個執行環境中,也有一個message函數,後者是接收來自worker對象在DOM級主線程中發送的消息,它有一個參數(暫定爲e),能夠經過e.data獲取傳遞的消息內容。

3. 另外,也還有一個postMessage函數,這個函數是向DOM級主線程中回發消息使用的,它的執行方向同worker對象的postMessage方法相反,並擁有相同類型的參數(暫定爲e)。

4. 這個執行上下文中有一個close方法,它的做用同worker對象的terminate方法類似,實現工做線程的自我關閉,不過worker對象沒有檢測工做線程是否已經關閉的方法,向一個已經關閉的工做線程發送消息將被忽略。

5. 由於這個執行上下文的獨立性,在工做線程腳本塊中,不可以訪問window對象以及建立它的DOM對象,可是它可使用許多其餘重要的構造函數,如XHR等,甚至能夠再次建立工做線程(worker對象的嵌套暫支持不夠普遍)。

6. 另外,若是工做線程腳本塊中須要依賴其餘腳本文件內容,須要跟上面例子中同樣,經過importScript函數引入使用。

  到目前爲止,從某個DOM級線程建立的各個worker對象是相互獨立的,沒有任何交集,欣喜的是,HTML5的API中定義了一個叫共享工做線程(share worker)的東西,它的出現讓這些獨立的worker有了交集,它提供一種計算服務給全部的工做線程,相似網絡socket,不過這個技術暫不被普遍支持,期待它普遍支持的那一天。

此外,值得一提的是,在worker的處理腳本中添加如下代碼:

1 while (true) {
2     console.log("1");
3 }

運行後發現,DOM級線程並無被阻塞,即瀏覽器並無卡死,可見,工做線程中的處理是徹底獨立於主程序運行環境以外的,這也符合了多線程的特徵,雖然在DOM級的多線程仍然沒法實現,但經過web worker ,腳本塊以前的多線程已經初見成效,這也是讓人欣慰的地方。

 

小結:隨着互聯網的進度,用戶對web的人機交互的需求愈來愈高,前端的異步技術在這個趨勢中發揮着重要的做用,web的開發者們也在竭盡全力的尋找新的、方便的、快捷的異步機制來追求更好的用戶體驗,上面三種方式均是對javascript異步多線程的嘗試,其中延時函數只能僞造多線程執行,有很大的侷限性,XHR過度依賴http,好像HTML5中的web worker在多線程的道路上取得了建設性進展,雖然它並無實現DOM級主程序中的多線程,但這是個很好的開端,但願接下來會愈來愈好~~~能力有限,歡迎你們指正~

相關文章
相關標籤/搜索