javascript運行環境和消息隊列

JavaScript虛擬機

JavaScript是一種解釋型語言,也就是說,它不須要編譯,能夠由解釋器實時運行。這樣的好處是運行和修改都比較方便,刷新頁面就能夠從新解釋;缺點是每次運行都要調用解釋器,系統開銷較大,運行速度慢於編譯型語言。爲了提升運行速度,目前的瀏覽器都將JavaScript進行必定程度的編譯,生成相似字節碼(bytecode)的中間代碼,以提升運行速度。node

早期,瀏覽器內部對JavaScript的處理過程以下:web

讀取代碼,進行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
對詞元進行語法分析(parsing),將代碼整理成「語法樹」(syntax tree)。
使用「翻譯器」(translator),將代碼轉爲字節碼(bytecode)。
使用「字節碼解釋器」(bytecode interpreter),將字節碼轉爲機器碼。
逐行解釋將字節碼轉爲機器碼,是很低效的。爲了提升運行速度,現代瀏覽器改成採用「即時編譯」(Just In Time compiler,縮寫JIT),即字節碼只在運行時編譯,用到哪一行就編譯哪一行,而且把編譯結果緩存(inline cache)。一般,一個程序被常常用到的,只是其中一小部分代碼,有了緩存的編譯結果,整個程序的運行速度就會顯著提高。ajax

不一樣的瀏覽器有不一樣的編譯策略。有的瀏覽器只編譯最常常用到的部分,好比循環的部分;有的瀏覽器索性省略了字節碼的翻譯步驟,直接編譯成機器碼,好比chrome瀏覽器的V8引擎。chrome

字節碼不能直接運行,而是運行在一個虛擬機(Virtual Machine)之上,通常也把虛擬機稱爲JavaScript引擎。由於JavaScript運行時未必有字節碼,因此JavaScript虛擬機並不徹底基於字節碼,而是部分基於源碼,即只要有可能,就經過JIT(just in time)編譯器直接把源碼編譯成機器碼運行,省略字節碼步驟。這一點與其餘採用虛擬機(好比Java)的語言不盡相同。這樣作的目的,是爲了儘量地優化代碼、提升性能。下面是目前最多見的一些JavaScript虛擬機:跨域

  • Chakra)(Microsoft Internet Explorer)
  • Nitro/JavaScript Core (Safari)
  • Carakan (Opera)
  • SpiderMonkey (Firefox)
  • V8) (Chrome, Chromium)

單線程模型

JavaScript採用單線程模型,也就是說,全部的任務都在一個線程裏運行。這意味着,一次只能運行一個任務,其餘任務都必須在後面排隊等待。瀏覽器

JavaScript之因此採用單線程,而不是多線程,跟歷史有關係。JavaScript從誕生起就是單線程,緣由是不想讓瀏覽器變得太複雜,由於多線程須要共享資源、且有可能修改彼此的運行結果,對於一種網頁腳本語言來講,這就太複雜了。好比,假定JavaScript同時有兩個線程,一個線程在某個DOM節點上添加內容,另外一個線程刪除了這個節點,這時瀏覽器應該以哪一個線程爲準?因此,爲了不復雜性,從一誕生,JavaScript就是單線程,這已經成了這門語言的核心特徵,未來也不會改變。緩存

爲了利用多核CPU的計算能力,HTML5提出Web Worker標準,容許JavaScript腳本建立多個線程,可是子線程徹底受主線程控制,且不得操做DOM。因此,這個新標準並無改變JavaScript單線程的本質。網絡

單線程模型帶來了一些問題,主要是新的任務被加在隊列的尾部,只有前面的全部任務運行結束,纔會輪到它執行。若是有一個任務特別耗時,後面的任務都會停在那裏等待,形成瀏覽器失去響應,又稱「假死」。爲了不「假死」,當某個操做在必定時間後仍沒法結束,瀏覽器就會跳出提示框,詢問用戶是否要強行中止腳本運行。數據結構

若是排隊是由於計算量大,CPU忙不過來,倒也算了,可是不少時候CPU是閒着的,由於IO設備(輸入輸出設備)很慢(好比Ajax操做從網絡讀取數據),不得不等着結果出來,再往下執行。JavaScript語言的設計者意識到,這時CPU徹底能夠無論IO設備,掛起處於等待中的任務,先運行排在後面的任務。等到IO設備返回告終果,再回過頭,把掛起的任務繼續執行下去。這種機制就是JavaScript內部採用的Event Loop。多線程

注:ajax同步異步可設置、用戶/瀏覽器自執行事件(onclick、onload、onkeyup等等)以及定時器(setTimeout、setInterval)是異步操做。

Event Loop

所謂Event Loop,指的是一種內部循環,用來排列和處理事件,以及執行函數。Wikipedia的定義是:「Event Loop是一個程序結構,用於等待和發送消息和事件。(a programming construct that waits for and dispatches events or messages in a program.)」

全部任務能夠分紅兩種,一種是同步任務(synchronous),另外一種是異步任務(asynchronous)。同步任務指的是,在主線程上排隊執行的任務,只有前一個任務執行完畢,才能執行後一個任務;異步任務指的是,不進入主線程、而進入「任務隊列」(task queue)的任務,只有「任務隊列」通知主線程,某個異步任務能夠執行了,該任務纔會進入主線程執行。

以Ajax操做爲例,它能夠看成同步任務處理,也能夠看成異步任務處理,由開發者決定。若是是同步任務,主線程就等着Ajax操做返回結果,再往下執行;若是是異步任務,該任務直接進入「任務隊列」,主線程跳過Ajax操做,直接往下執行,等到Ajax操做有告終果,主線程再執行對應的回調函數。

想要理解Event Loop,就要從程序的運行模式講起。運行之後的程序叫作"進程"(process),通常狀況下,一個進程一次只能執行一個任務。若是有不少任務須要執行,不外乎三種解決方法。

  1. 排隊。由於一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行後面的任務。
  2. 新建進程。使用fork命令,爲每一個任務新建一個進程。
  3. 新建線程。由於進程太耗費資源,因此現在的程序每每容許一個進程包含多個線程,由線程去完成任務。

若是某個任務很耗時,好比涉及不少I/O(輸入/輸出)操做,那麼線程的運行大概是下面的樣子。

2013102002.png

上圖的綠色部分是程序的運行時間,紅色部分是等待時間。能夠看到,因爲I/O操做很慢,因此這個線程的大部分運行時間都在空等I/O操做的返回結果。這種運行方式稱爲"同步模式"(synchronous I/O)。

若是採用多線程,同時運行多個任務,那極可能就是下面這樣。

2013102003.png

上圖代表,多線程不只佔用多倍的系統資源,也閒置多倍的資源,這顯然不合理。

2013102004.png

上圖主線程的綠色部分,仍是表示運行時間,而橙色部分表示空閒時間。每當遇到I/O的時候,主線程就讓Event Loop線程去通知相應的I/O程序,而後接着日後運行,因此不存在紅色的等待時間。等到I/O程序完成操做,Event Loop線程再把結果返回主線程。主線程就調用事先設定的回調函數,完成整個任務。

能夠看到,因爲多出了橙色的空閒時間,因此主線程得以運行更多的任務,這就提升了效率。這種運行方式稱爲"異步模式"(asynchronous I/O)。

這正是JavaScript語言的運行方式。單線程模型雖然對JavaScript構成了很大的限制,但也所以使它具有了其餘語言不具有的優點。若是部署得好,JavaScript程序是不會出現堵塞的,這就是爲何node.js平臺能夠用不多的資源,應付大流量訪問的緣由。

任務隊列

若是有大量的異步任務(實際狀況就是這樣),它們會在「任務隊列」中註冊大量的事件。這些事件排成隊列,等候進入主線程。本質上,「任務隊列」就是一個事件「先進先出」的數據結構。好比,點擊鼠標就產生一些列事件,mousedown事件排在mouseup事件前面,mouseup事件又排在click事件的前面。

事件循環

之因此稱爲 事件循環,是由於它常常被用於相似以下的方式來實現:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

若是當前沒有任何消息,queue.waitForMessage 會等待着同步將要到來的消息。

"執行至完成"

每個消息執行完成後,其它消息纔會被執行。當你分析你的程序時,這點提供了一些優秀的特性,包括每當一個函數運行時,它就不能被搶佔,而且在其餘代碼運行以前徹底運行(且能夠修改此函數控制的數據)。這點與C語言不一樣。例如,C語言中當一個程序在一個線程中運行時,它能夠在任何點中止且能夠在其它線程中運行其它代碼。

這個模型的一個缺點在於當一個消息的完成耗時過長,網絡應用沒法處理用戶的交互如點擊或者滾動。瀏覽器用「程序須要過長時間運行」的對話框來緩解這個問題。一個比較好的解決方案是使消息處理變短且若是可能的話,將一個消息拆分紅幾個消息。

添加消息

在瀏覽器裏,當一個事件出現且有一個事件監聽器被綁定時,消息會被隨時添加。若是沒有事件監聽器,事件會丟失。因此點擊一個附帶點擊事件處理函數的元素會添加一個消息。其它事件亦然。

調用 setTimeout 函數會在一個時間段過去後在隊列中添加一個消息。這個時間段做爲函數的第二個參數被傳入。若是隊列中沒有其它消息,消息會被立刻處理。可是,若是有其它消息,setTimeout 消息必須等待其它消息處理完。所以第二個參數僅僅表示最少的時間 而非確切的時間。

零延遲

零延遲 (Zero delay) 並非意味着回調會當即執行。在零延遲調用 setTimeout 時,其並非過了給定的時間間隔後就立刻執行回調函數。其等待的時間基於隊列里正在等待的消息數量。在下面的例子中,"this is just a message" 將會在回調 (callback) 得到處理以前輸出到控制檯,這是由於延遲是要求運行時 (runtime) 處理請求所需的最小時間,但不是有所保證的時間。

(function () {

  console.log('this is the start');

  setTimeout(function cb() {
    console.log('this is a msg from call back');
  });

  console.log('this is just a message');

  setTimeout(function cb1() {
    console.log('this is a msg from call back1');
  }, 0);

  console.log('this is the  end');

})();

// "this is the start"
// "this is just a message"
// "this is the end"
// "this is a msg from call back"
// "this is a msg from call back1"

多個運行時互相通訊

一個 web worker 或者一個跨域的 iframe 都有它們本身的棧,堆和消息隊列。兩個不一樣的運行時只有經過 postMessage 方法進行通訊。這個方法會給另外一個運行時添加一個消息若是後者監聽了 message 事件。

相關文章
相關標籤/搜索