javascript實現的非同步功能詳解

admin發表於2017-04-05

本章節詳細介紹一下javascript非同步功能的相關問題。

需要的朋友可以做一下參考,廢話不多說,下面進入正題:

一. 先看下兩個例子:

(1).簡單的settimeout:

[JavaScript] 純文字檢視 複製程式碼
setTimeout(function () { while (true) { } }, 1000);
setTimeout(function () { console.log('end 2'); }, 2000);
setTimeout(function () { console.log('end 1'); }, 100);
console.log('end');

執行的結果是輸出’end’、’end 1’,然後瀏覽器假死,就是不彈出‘end 2’。

也就是說第一個settimeout裡執行的時候是一個死迴圈,這個直接導致了理論上比它晚一秒執行的第二個settimeout裡的函式被阻塞,這個和我們平時所理解的非同步函式多執行緒互不干擾是不符的。

附計時器使用方法:

[JavaScript] 純文字檢視 複製程式碼
//初始化一個簡單的js的計時器,一段時間後,才觸發並執行回撥函式。 
//setTimeout 返回一個唯一id,可用這個id來取消這個計時器。
var id = setTimeout(fn,delay);
//類似於setTimeout,不一樣的是,每隔一段時間,會持續呼叫回撥fn,直到被取消
var id = setInterval(fn,delay);
//傳入一個計時器的id,取消計時器。
clearInterval(id);
clearTimeout(id);

(2).ajax請求回撥:

接著我們來測試一下通過xmlhttprequest實現ajax非同步請求呼叫,主要程式碼如下:

[JavaScript] 純文字檢視 複製程式碼
var xmlReq = createXMLHTTP();//建立一個xmlhttprequest物件
function testAsynRequest() {
    var url = "/AsyncHandler.ashx?action=ajax";
    xmlReq.open("post", url, true);
    xmlReq.setRequestHeader("Content-Type", "application/x-www-form-urlencoded");
    xmlReq.onreadystatechange = function () {
        if (xmlReq.readyState == 4) {
            if (xmlReq.status == 200) {
                var jsonData = eval('(' + xmlReq.responseText + ')');
                alert(jsonData.message);
            }
            else if (xmlReq.status == 404) {
                alert("Requested URL is not found.");
            } else if (xmlReq.status == 403) {
                alert("Access denied.");
            } else {
                alert("status is " + xmlReq.status);
            }
        }
    };
    xmlReq.send(null);
}
testAsynRequest();//1秒後呼叫回撥函式
  
while (true) {
}

在服務端實現簡單的輸出:

[C#] 純文字檢視 複製程式碼
private void ProcessAjaxRequest(HttpContext context)
{
    string action = context.Request["ajax"];
    Thread.Sleep(1000);//等1秒
    string jsonObject = "{\"message\":\"" + action + "\"}";
    context.Response.Write(jsonObject);
}

理論上,如果ajax非同步請求,它的非同步回撥函式是在單獨一個執行緒中,那麼回撥函式必然不被其他執行緒”阻撓“而順利執行,也就是1秒後,它回撥執行彈出‘ajax’,可是實際情況並非如此,回撥函式無法執行,因為瀏覽器再次因為死迴圈假死。

據上面兩個例子,總結如下:

①.javascript引擎是單執行緒執行的,瀏覽器無論在什麼時候都只且只有一個執行緒在執行JavaScript程式。

②.javascript引擎用單執行緒執行也是有意義的,單執行緒不必理會執行緒同步這些複雜的問題,問題得到簡化。

二.JavaScript引擎:

在瞭解計時器內部運作前,我們必須清楚一點,觸發和執行並不是同一概念,計時器的回撥函式一定會在指定delay的時間後被觸發,但並不一定立即執行,可能需要等待。所有JavaScript程式碼是在一個執行緒裡執行的,像滑鼠點選和計時器之類的事件只有在JS單執行緒空閒時才執行。

我們來看一下圖表,一開始你可能並沒發現什麼或啥都不懂,但請靜下心來,在腦海裡繪製出這個場景

a:3:{s:3:\"pic\";s:43:\"portal/201704/05/175320ota8etolr9ja1s1e.jpg\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

這個圖表中有許多資料資訊等著我們去理解,當你完全理解了這個圖,你會對js的非同步執行機制(即JavaScript引擎如何實現非同步事件)有很好的瞭解。這個圖是一維的,垂直線上是以毫秒計位,藍色塊代表被劃分的不同的js區域執行程式碼。例如,第一個JS區塊執行了18毫秒,滑鼠點選事件被阻塞了將近11毫秒,等等。

    由於JavaScript引擎同一時間只執行一段程式碼(這是由JavaScript單執行緒的性質決定的),所以每個JS程式碼塊阻塞了其它非同步事件的進行。這意味著當一個非同步事件(像滑鼠點選、計時器、Ajax)發生時,這些事件的回撥函式將排在佇列後面等待執行(如何排隊完全取決於各瀏覽器,而我們可以忽視它們內部差異,作一個簡化處理)。 


    我們首先從第一個JS程式碼塊開始,有兩個計時器被初始化:一個10ms的setTimeout和一個10ms的setInterval.觀察計時器初始化位置,(計時器初始化完畢後就會開始計時),發現setTimeout計時器的回撥實際上會在第一個程式碼塊執行完畢前被觸發。但是這裡注意的是,它不會立即執行(單執行緒不能這樣做)。實際上,觸發的回撥將被排成一個佇列,等待下一個可執行時間。


    此外,在第一個JS程式碼塊,我們發現一個滑鼠點選事件被觸發。這個滑鼠點選JS回撥被繫結在非同步佇列上(我們從來不知道使用者什麼時候執行這個操作,所以它被認為是非同步的)且不能馬上執行。像初始化的計時器一樣,排隊等待執行。


    執行完初始化JS程式碼塊後,瀏覽器就有個疑問:誰在等待執行?此時,滑鼠點選回撥和setTimeout計時器的回撥都在等待。瀏覽器將選一個(滑鼠點選事件)並立馬執行。而計時器的回撥將等待下一合適時機執行。


    注意,滑鼠點選事件執行過程中,interval的回撥第一次被觸發,與setTimeout的回撥一樣,排隊等待執行。隨著時間推移,等到setTimeout計時器的回撥執行時候,setInterval的回撥再次被觸發,這次被觸發的回撥將被拋棄。如果一大段程式碼塊正在執行,所有的setInterval的回撥都將要排隊,一旦大段程式碼塊執行完畢,這些一連串的setInterval的回撥相互間將被無延遲地執行。實際上,瀏覽器處理setInterval被觸發的回撥排隊等待執行時,除非佇列中setInterval回撥為空,才允許新的setInterval的回撥加入。


    我們發現,setInterval的第一個被觸發的回撥執行時,setInterval的回撥又被觸發且排到佇列。這向我們傳達一個重要的訊息:setInterval不關心目前JS正在執行的內容,setInterval的被觸發的回撥都將會無差別地排隊。


 最後,當setInterval的回撥執行兩次後,我們發現沒有javascript引擎要執行東西。這意味著瀏覽器將等待著一個新的非同步事件發生。我們知道,在50ms時候,setInterval的回撥再次被觸發,但這次並沒有東西阻塞,所以回撥就立馬執行了。 


 在瀏覽器中,JavaScript引擎是基於事件驅動的,這裡的事件可看作是瀏覽器派給它的各種任務,這些任務可能源自當前執行的程式碼塊,如呼叫setTimeout(),也可能來自瀏覽器核心,如onload()、onclick()、onmouseover()、setTimeOut()、setInterval()、Ajax等。如果從程式碼的角度來看,所謂的任務實體就是各種回撥函式,由於“單執行緒”的原因,這些任務會進行排隊,一個接著一個等待著被引擎處理。

三.javascript引擎執行緒和其它偵聽執行緒:

a:3:{s:3:\"pic\";s:43:\"portal/201704/05/175357tjoz19xwsjoon15q.jpg\";s:5:\"thumb\";s:0:\"\";s:6:\"remote\";N;}

上圖中,定時器和事件都按時觸發了,這表明JavaScript引擎的執行緒和計時器觸發執行緒、事件觸發執行緒是三個單獨的執行緒,即使JavaScript引擎的執行緒被阻塞,其它兩個觸發執行緒都在執行。

    瀏覽器核心實現允許多個執行緒非同步執行,這些執行緒在核心制控下相互配合以保持同步。假如某一瀏覽器核心的實現至少有三個常駐執行緒: JavaScript引擎執行緒,事件觸發執行緒,Http請求執行緒,下面通過一個圖來闡明單執行緒的JavaScript引擎與另外那些執行緒是怎樣互動通訊的。雖然每個瀏覽器核心實現細節不同,但這其中的呼叫原理都是大同小異。

    執行緒間通訊:JavaScript引擎執行當前的程式碼塊,其它諸如setTimeout給JS引擎新增一個任務,也可來自瀏覽器核心的其它執行緒,如介面元素滑鼠點選事件,定時觸發器時間到達通知,非同步請求狀態變更通知等.從程式碼角度看來任務實體就是各種回撥函式,JavaScript引擎一直等待著任務佇列中任務的到來.由於單執行緒關係,這些任務得進行排隊,一個接著一個被引擎處理.

    GUI渲染也是在引擎執行緒中執行的,指令碼中執行對介面進行更新操作,如新增結點,刪除結點或改變結點的外觀等更新並不會立即體現出來,這些操作將儲存在一個佇列中,待JavaScript引擎空閒時才有機會渲染出來。來看例子(這塊內容還有待驗證,個人覺得當Dom渲染時,才可阻止渲染)

[HTML] 純文字檢視 複製程式碼
<div id="test">test</div>
<script type="text/javascript" language="javascript">
var i=0;
while(1) {
    document.getElementById("test").innerHTML+=i++ + "<br />";
}
</script>

這段程式碼的本意是從0開始順序顯示數字,它們將一個接一個出現,現在我們來仔細研究一下程式碼,while(1)建立了一個無休止的迴圈,但是對於單執行緒的JavaScript引擎而言,在實際情況中就會造成瀏覽器暫停響應並處於假死狀態。

alert()會停止JS引擎的執行,直到按確認鍵,在JS除錯的時候,檢視當前實時頁面的內容。

四.setTimeout和 setInterval:

回到文章開頭,我們來看下setTimeout和setsetInterval的區別。

[JavaScript] 純文字檢視 複製程式碼
setTimeout(function(){
    /* Some long block of code ... */
    setTimout(arguments.callee,10);
},10);
 
setInterval(function(){
    /* Some long block of code ... */
},10);

這兩個程式段第一眼看上去是一樣的,但並不是這樣。setTimeout程式碼至少每隔10ms以上才執行一次;然而setInterval固定每隔10ms將嘗試執行,不管它的回撥函式的執行狀態。

我們來總結下:

(1). JavaScript引擎只有一個執行緒,強制非同步事件排隊等待執行。

(2).setTimeout和setInterval在非同步執行時,有著根本性不同。

(3).如果一個計時器被阻塞執行,它將會延遲,直到下一個可執行點(這可能比期望的時間更長)。

(4).setInterval的回撥可能被不停的執行,中間沒間隔(如果回撥執行的時間超過預定等待的值)。

《JavaScript高階程式設計》中,針對setInterval說法如下:

當使用setInterval()時,僅當沒有該定時器的任何其他程式碼例項時,才將定時器程式碼新增到佇列中。還要注意兩問題:

(1).些間隔會被跳過(拋棄);

(2).多個定時器的程式碼執行之間的間隔可能會比預期小。此時可採取 setTimeout和setsetInterval的區別 的例子方法。

五.Ajax非同步:

很多同學朋友搞不清楚,既然說JavaScript是單執行緒執行的,那麼XMLHttpRequest在連線後是否真的非同步。

其實請求確實是非同步的,不過這請求是由瀏覽器新開一個執行緒請求(參見上圖),當請求的狀態變更時,如果先前已設定回撥,這非同步執行緒就產生狀態變更事件放到JavaScript引擎的處理佇列中等待處理,當任務被處理時,JavaScript引擎始終是單執行緒執行回撥函式,具體點即還是單執行緒執行onreadystatechange所設定的函式。

相關文章