最近在搜索更詳細的關於JS事件處理的資料。發現國內的大部分blog都是相互抄襲。而MDM對於這裏的解釋也並很少。翻閱了部分文章,發現這篇文章頗有價值。故譯之。雖然文章寫於2013年,可是依然具備很高參考價值javascript
原文:The JavaScript Event Loop: Explained java
對於目前Web
瀏覽器上最流行的腳本語言JavaScript
。這篇文章爲你提供了該語言基本的事件驅動模型的講解,它與那些典型的有求必應的語言好比Ruby
, Python
, Java
不一樣。在這篇文章中,我會爲你解釋這些JavaScript
中併發模型的核心概念,包括事件循環,消息隊列來幫助你提升對這門已經使用過,可是尚未完全理解的語言更深刻的理解。瀏覽器
這篇文章面向那些已經開始彷佛用JavaScript
語言從事Web
開發的工程師,或者是計劃從事這項工做的人員。若是你已經很是熟悉JavaScript
的事件循環機制,那麼你會以爲這篇文章的內容對於你來講已經再熟悉不過了。對於那些沒有對事件循環充分了解的人,我但願這篇文章可以幫助到你,這樣才能讓你更好的理解你天天面對的代碼。ruby
再JavaScript
幾乎全部的I/O都是非阻塞的。包括HTTP
請求,數據訪問,讀寫磁盤。一個單線程在運行時去處理這些操做,提供一個回調函數,而後接着去作其它的事情。當操做完成了,這個回調函數提供的消息會被推送到隊列中。在某個時間點,消息從隊列中被移除,緊接着回調函數就被觸發了。服務器
雖然這個交互模型對於不少開發者來講已經很是熟悉了 -- 好比 mousedown
和 click
事件的處理,- 可是這與那種典型服務器端同步的請求處理不一樣。閉包
讓咱們對比一下向 www.google.com
發出請求後將返回的代碼輸出到控制檯。首先,Ruby
的話:併發
response = Faraday.get 'http://www.google.com'
puts response
puts 'Done!'
複製代碼
執行路徑大概是這個樣子的:異步
get
方法被執行,而後執行線程開始等待,直到收到響應Google
收到響應而且返回到回調並存儲到一個變量中Done
被輸出到控制檯讓咱們利用 Node.js
中完成同樣的事情看看:函數
request('http://www.google.com', function(error, response, body) {
console.log(body);
});
console.log('Done!');
複製代碼
看起來一個顯著的不一樣和不一樣的行爲:oop
Done
被立刻輸出到控制套將請求的響應以回調函數的方式處理,容許 JavaScript
在等待異步操做成功返回並執行回調函數以前能夠作一些其餘的事情。可是,在內存中怎麼執行這些回調的呢?執行順序是什麼樣的呢?什麼致使他們被調用的呢?
JavaScript
的運行環境有一個用於存儲消息和用於關聯回調函數的消息隊列。這些消息以事件被註冊的順序進行排列(好比鼠標點擊事件或者是 HTTP
請求響應事件)。好比用戶點擊一個按鈕,若是沒有該事件的回調函數被註冊,那麼就沒有消息加入隊列。
在循環中,隊列輪詢下一個消息(每一次輪詢被看成是一個 tick
),若是有消息,那麼就執行消息對應的回調。
回調函數的調用做爲調用堆棧的初始幀,因爲JavaScript
是單線程的,消息輪詢和處理會被中止,直到堆棧內的回調函數所有返回。後續函數調用(同步)向堆棧添加新的調用(例如初始化顏色)。
function init() {
var link = document.getElementById("foo");
link.addEventListener("click", function changeColor() {
this.style.color = "burlywood";
});
}
init();
複製代碼
在這個例子中,當用戶點擊頁面元素,而後一個onclick
事件被觸發,一個消息被壓入隊列中。當消息被壓入隊列,他的回調函數changeColor
被執行。當changeColor
返回的時候(也多是拋出異常),事件循環就繼續執行。只要changeColor
被指定爲onclick
的回調函數,後面在該元素的點擊會致使更多的消息(以及相關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
是非阻塞的,它會在0毫秒後被執行,而且並非做爲此消息的一部分被處理。在這個例子中,setTimeout
被調用,傳入一個回調函數 g
和 一個超時事件 0 毫秒。當時間到了之後,一個以g
爲回調函數的消息將會被壓入隊列中。控制檯會輸出相似: foo
, baz
, blix
而後在下一次事件循環輸出: bar
。若是在同一個調用幀中(譯者注:就是一個函數內)執行了兩次 setTimeout
,傳入相同的值(譯者注: 時間間隔)。他們會按照前後順序執行。
Web Worker
容許你將昂貴的操做轉入到獨立到線程中執行,節約主要線程去作其它事情。worker
具備獨立的消息隊列,事件循環,和獨立的內存空間。worker
與主線程經過消息來完成通信,這看起來有點像以前的事件處理那樣。
首先,咱們的 worker
:
// our worker, which does some CPU-intensive operation
var reportResult = function(e) {
pi = SomeLib.computePiToSpecifiedDecimals(e.data);
postMessage(pi);
};
onmessage = reportResult;
複製代碼
而後是咱們的js
代碼:
// 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
被註冊到本身的message
事件中。當worker
從主線程接收到消息,worker
就會返回一個消息,所以就會致使reportResult
被執行。
當進行壓棧的時候,一個消息會被推送到主線程,並被壓入消息堆棧(而後執行回調函數)。經過這個方式,開發人員能夠將cpu密集型操做委託給獨立的線程,釋放主線程去繼續處理消息和事件。
JavaScript
支持在回調函數中使用閉包,該回調在執行時維持對建立它們的環境的訪問,即便回調執行完建立了新的調用堆棧。對於知道回調是以不一樣的消息被執行的要比知道回調被建立更有趣。思考下面的代碼:
function changeHeaderDeferred() {
var header = document.getElementById("header");
setTimeout(function changeHeader() {
header.style.color = "red";
return false;
}, 100);
return false;
}
changeHeaderDeferred();
複製代碼
這個例子中,changeHeaderDeferred
執行的時候包含了header
變量。setTimeout
被執行,100毫秒後一個消息被添加到消息隊列中。changeHeaderDeferred
函數返回了false,結束了此次處理 - 可是回調函數中依然保留着header
的引用,因此沒有被垃圾回收。當第二個消息被處理的時候,函數體外(changeHeaderDeferred
)它依然保持這對header
的聲明。第二次處理完後,header
才被垃圾回收處理。