這篇文章關于什么?
JAVAscript作為瀏覽器腳本語言,已經逐漸變得無處不在,它讓你對事件驅動模型有了基本理解,以及它與request-response模型的典型語言,如Ruby,Python和Java的區別,我將闡述一些關于JavaScript一致的核心概念,包括它的事件輪詢和消息隊列,希望能幫助你理解這門或許你不是完全理解的語言。
致讀者:
這篇文章寫給準備使用Javascript進行服務端/客戶端開發的web開發工作者(或者準備從事該職業者),如果你對事件輪詢機制有比較好的了解,這篇文章或許你會覺得很熟悉。對于不是很了解事件輪詢機制的讀者,我希望能幫助你對這每天讀寫的代碼有一個基本的理解。
非阻塞I/O
在Javascript中,幾乎所有的I/O都是非阻塞的,其中包括http請求,數據操作以及磁盤讀寫;單線程詢問任務運行時機以及執行任務,通過使用回調函數,能讓Javascript線程在回調完成之前執行其它任務。當一個執行完成的時候,會去執行由回調函數提供的有序消息隊列中的下一個任務。
當開發者熟悉了這種交互模式,用戶習慣了這種界面 — 當事件發生,例如“mousedown”,“click”這種隨時可能被觸發的事件,它不同于同步機制,請求-響應模型很少用在服務端應用上。
讓我們比較兩塊代碼,它們分別發起HTTP請求于www.google.com然后在控制臺輸出響應結果。首先,Ruby,使用Faraday:
response = Faraday.get 'http://www.google.com' puts response puts 'Done!'
執行結果如下所示:
get方法已執行,然而該線程直到有響應才被回收
該來自Google的響應返回的數據并沒有存在變量中
響應結果輸出在控制臺中
直至最后,“任務完成”才出現在控制臺中
讓我們用Javascript的Node.js及它的Request庫來做同樣的事:
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
請求已被執行,在請求得到響應之前則已跳過一個匿名回調函數(該函數并未執行)
“任務完成”馬上出現在控制臺中
一段時間之后,收到響應,此時回調函數才被執行—在控制臺輸出響應結果
事件輪詢
非耦合機制使得Javascript線程能在等待異步操作完成及其回調函數執行之前執行其它任務。那么,在內存中在哪激活回調?回調按什么規則執行?什么會讓回調執行呢?
Javascript線程包括一個儲存了待執行任務的消息列表的消息隊列,以及與它們相關聯的回調函數,這些消息按照它們的響應順序排列(例如鼠標點擊,或者收到來自HTTP請求的響應)每條消息都有回調函數,如果沒有提供回調函數:例如當用戶點擊一個按鈕但是沒有提供回調函數,則沒有消息會被添加到消息隊列。
在每一次輪詢中,任務隊列會記錄下一條消息(每次記錄會返回一個“tick”),當輪詢到這條消息時,該消息所對應的回調函數則被執行。
在最初的架構中,回調函數通過調用棧來實現,由于Javascript是單線程的,消息隊列是阻塞的,對于后續任務,必須等待之前的任務返回棧中所有回調函數,才能將新任務的回調函數加入到棧中。在隨后的架構中加入了函數(同步的)對棧的新的調用方法(此處例舉一個初始化為changeColor的函數)。
function init() {
var link = document.getElementById("foo");
link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}
init();
在這個例子中,當用戶點擊“foo”這個元素,“onclick”事件被觸發,一個消息(以及回調函數changeColor)加入消息隊列。當隊列按序執行到該消息時,它的回調函數changeColor被喚起。
當回調函數changeColor返回(或者出錯被丟掉),事件輪詢繼續執行。只要與”foo”元素的onclick事件綁定的回調函數changeColor存在,隨后在該元素上的click事件都會使得更多的消息(及其回調函數changeColor)被加入隊列。
消息隊列的添加
如果你申明了一個異步函數(例如setTimeout),其回調函數最終會在一個不同的消息隊列中執行,在未來的事件輪詢中的某個時刻。例如:
function f() {
console.log("foo");
setTimeout(g, 0);
console.log("baz");
h();
}
function g() {
console.log("bar");
}
function h() {
console.log("blix");
}
f();
由于setTimeout非阻塞的本質,它的回調函數的未來的若干毫秒后執行并且等待期間不占用該消息的進程。在這個例子中,setTimeout跳過它的回調函數g和一段事件的延遲后被喚起。當預先聲明的時間結束(在這個例子中幾乎是立即執行)被分離出去的消息又被重新加回隊列,包括其回調函數g。這個回調函數被激活就好比:”foo”,”baz”,”blix”然后執行下一個事件輪詢的tick:”bar”。如果在一個框架中同時聲明了兩個setTimeout,并且他們的第二個參數(執行時間)想同,他們的回調將會按照其定義順序執行。
Web Workers
利用Web Workers能讓你丟掉昂貴的多線程執行方式,釋放主線程去做其他的事。Web Workers包括單獨的消息隊列,事件輪詢,以及實例化了一個獨立于最初的主線程的儲存空間。利用消息傳遞來建立消息與主線程之間的聯系,這種聯系非常像我們剛才的代碼示例。
首先,我們的worker:
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
然后,這是在html中的代碼內容:
// our main code, in a <script>-tag in our HTML page
var piWorker = new Worker("pi_calculator.js");
var logResult = function(e) {
console.log("PI: " + e.data);
};
piWorker.addEventListener("message", logResult, false);
piWorker.postMessage(100000);
該示例中,主線程產生一個worker然后將一個logResult回調函數注冊到消息隊列中。在worker中,reportResult函數被注冊到它自己的消息事件中。當worker線程從主線程接收消息時,worker將消息及其相應的回調函數加入隊列中。當消息隊列按順序執行到該消息時,主線程將發回一條消息并將一條新的消息加入隊列(按照logResult的回調排序)由此開發者能讓CPU集中處理分線程,釋放主線程繼續處理消息任務及其綁定事件。
關于閉包
Javascript支持閉包,準許注冊回調,當我們執行回調時,通過執行回調創造的新的完全調用棧來維持我們創造的環境的入口。回調函數作為不同于我們創造的消息的一部分被調用。考慮如下示例:
function changeHeaderDeferred() {
var header = document.getElementById("header");
setTimeout(function changeHeader() {
header.style.color = "red";
return false;
}, 100);
return false;
}
changeHeaderDeferred();
在這個示例中,以頭變量方式聲明的changeHeaderDeferred函數被執行。setTimeout函數被喚醒,導致消息(加在changeHeader回調中的)大約在100毫秒之后(時間偏差源于每臺計算機內置原子鐘差異)添加到消息隊列,changeHeaderDeferred返回false,結束第一條消息的進程,然而頭變量依然通過閉包的方式存在,沒有被垃圾回收機制回收。當第二條消息執行(changeHeader函數)維持頭變量聲明的外部函數域的入口。一旦第二條消息(changeHeader函數)執行完畢,頭變量則被回收。
另外
Javascript的事件驅動交互模型不同于大多數編程人員習慣的請求-響應模型,但是你能看到,該技術也不是那么高不可攀。一個簡單的消息隊列及事件輪詢,Javascript使得開發者能夠圍繞收集異步回調的形式來建立他們的系統,在等待外部事件發生的同時釋放主線程去做其它操作。它將越來越流行。
希望本文能幫助到您!
點贊+轉發,讓更多的人也能看到這篇內容(收藏不點贊,都是耍流氓-_-)
關注 {我},享受文章首發體驗!
每周重點攻克一個前端技術難點。更多精彩前端內容私信 我 回復“教程”
原文鏈接:http://eux.baidu.com/blog/fe/javascript-loop
作者:erin






