瀏覽器渲染基本原理(二):JS引擎的工做方式

JS引擎也能夠叫作JS解釋器node

瀏覽器的組成

瀏覽器的核心是兩部分:渲染引擎和JavaScript解釋器(又稱JavaScript引擎)。chrome

(1)渲染引擎promise

渲染引擎的主要做用是,將網頁從代碼「渲染」爲用戶視覺上能夠感知的平面文檔。不一樣的瀏覽器有不一樣的渲染引擎。瀏覽器

以上四步並不是嚴格按順序執行,每每第一步還沒完成,第二步和第三步就已經開始了。因此,會看到這種狀況:網頁的HTML代碼還沒下載完,但瀏覽器已經顯示出內容了。緩存

(2)JavaScript引擎網絡

JavaScript引擎的主要做用是,讀取網頁中的JavaScript代碼,對其處理後運行。數據結構

本節主要介紹JavaScript引擎的工做方式。多線程

 

 

<script>標籤的工做原理

 

正常的網頁加載流程是這樣的。

  1. 瀏覽器一邊下載HTML網頁,一邊開始解析
  2. 解析過程當中,發現<script>標籤
  3. 暫停解析,網頁渲染的控制權轉交給JavaScript引擎
  4. 若是<script>標籤引用了外部腳本,就下載該腳本,不然就直接執行
  5. 執行完畢,控制權交還渲染引擎,恢復往下解析HTML網頁

 

也就是說,加載外部腳本時,瀏覽器會暫停頁面渲染,等待腳本下載並執行完成後,再繼續渲染。緣由是JavaScript能夠修改DOM(好比使用document.write方法),因此必須把控制權讓給它,不然會致使複雜的線程競賽的問題。異步

若是外部腳本加載時間很長(好比一直沒法完成下載),就會形成網頁長時間失去響應,瀏覽器就會呈現「假死」狀態,這被稱爲「阻塞效應」。async

爲了不這種狀況,較好的作法是將<script>標籤都放在頁面底部,而不是頭部。這樣即便遇到腳本失去響應,網頁主體的渲染也已經完成了,用戶至少能夠看到內容,而不是面對一張空白的頁面。

若是某些腳本代碼很是重要,必定要放在頁面頭部的話,最好直接將代碼嵌入頁面,而不是鏈接外部腳本文件,這樣能縮短加載時間。

將腳本文件都放在網頁尾部加載,還有一個好處。在DOM結構生成以前就調用DOM,JavaScript會報錯,若是腳本都在網頁尾部加載,就不存在這個問題,由於這時DOM確定已經生成了。

 

<head><script>console.log(document.body.innerHTML);
</script></head><body></body>

上面代碼執行時會報錯,由於此時document.body元素還未生成。

一種解決方法是設定DOMContentLoaded事件的回調函數。

 

 

 

下面是一個window.requestAnimationFrame()對比效果的例子。

// 重繪代價高functiondoubleHeight(element) {
varcurrentHeight=element.clientHeight;
element.style.height= (currentHeight*2) +'px';
}
all_my_elements.forEach(doubleHeight);
// 重繪代價低functiondoubleHeight(element) {
varcurrentHeight=element.clientHeight;
window.requestAnimationFrame(function () {
element.style.height= (currentHeight*2) +'px';
});
}
all_my_elements.forEach(doubleHeight);

 

 

 

 

 

JavaScript虛擬機

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

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

  1. 讀取代碼,進行詞法分析(Lexical analysis),將代碼分解成詞元(token)。
  2. 對詞元進行語法分析(parsing),將代碼整理成「語法樹」(syntax tree)。
  3. 使用「翻譯器」(translator),將代碼轉爲字節碼(bytecode)。
  4. 使用「字節碼解釋器」(bytecode interpreter),將字節碼轉爲機器碼。

逐行解釋將字節碼轉爲機器碼,是很低效的。爲了提升運行速度,現代瀏覽器改成採用「即時編譯」(Just In Time compiler,縮寫JIT),即字節碼只在運行時編譯,用到哪一行就編譯哪一行,而且把編譯結果緩存(inline cache)。一般,一個程序被常常用到的,只是其中一小部分代碼,有了緩存的編譯結果,整個程序的運行速度就會顯著提高。

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

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

 

單線程模型

含義

首先,明確一個觀念:JavaScript只在一個線程上運行,不表明JavaScript引擎只有一個線程。事實上,JavaScript引擎有多個線程,其中單個腳本只能在一個線程上運行,其餘線程都是在後臺配合。JavaScript腳本在一個線程裏運行。這意味着,一次只能運行一個任務,其餘任務都必須在後面排隊等待。

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

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

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

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

消息隊列

JavaScript運行時,除了一根運行線程,系統還提供一個消息隊列(message queue),裏面是各類須要當前程序處理的消息。新的消息進入隊列的時候,會自動排在隊列的尾端。

運行線程只要發現消息隊列不爲空,就會取出排在第一位的那個消息,執行它對應的回調函數。等到執行完,再取出排在第二位的消息,不斷循環,直到消息隊列變空爲止。

每條消息與一個回調函數相聯繫,也就是說,程序只要收到這條消息,就會執行對應的函數。另外一方面,進入消息隊列的消息,必須有對應的回調函數。不然這個消息就會遺失,不會進入消息隊列。舉例來講,鼠標點擊就會產生一條消息,報告click事件發生了。若是沒有回調函數,這個消息就遺失了。若是有回調函數,這個消息進入消息隊列。等到程序收到這個消息,就會執行click事件的回調函數。

另外一種狀況是setTimeout會在指定時間向消息隊列添加一條消息。若是消息隊列之中,此時沒有其餘消息,這條消息會當即獲得處理;不然,這條消息會不得不等到其餘消息處理完,纔會獲得處理。所以,setTimeout指定的執行時間,只是一個最先可能發生的時間,並不能保證必定會在那個時間發生。

一旦當前執行棧空了,消息隊列就會取出排在第一位的那條消息,傳入程序。程序開始執行對應的回調函數,等到執行完,再處理下一條消息。

 

Event Loop

所謂Event Loop,指的是一種內部循環,用來一輪又一輪地處理消息隊列之中的消息,即執行對應的回調函數。Wikipedia的定義是:「Event Loop是一個程序結構,用於等待和發送消息和事件(a programming construct that waits for and dispatches events or messages in a program)」。能夠就把Event Loop理解成動態更新的消息隊列自己。

下面是一些常見的JavaScript任務。

  • 執行JavaScript代碼
  • 對用戶的輸入(包含鼠標點擊、鍵盤輸入等等)作出反應
  • 處理異步的網絡請求

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

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

也就是說,雖然JavaScript只有一根進程用來執行,可是並行的還有其餘進程(好比,處理定時器的進程、處理用戶輸入的進程、處理網絡通訊的進程等等)。這些進程經過向任務隊列添加任務,實現與JavaScript進程通訊。

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

  1. 排隊。由於一個進程一次只能執行一個任務,只好等前面的任務執行完了,再執行後面的任務。

  2. 新建進程。使用fork命令,爲每一個任務新建一個進程。

  3. 新建線程。由於進程太耗費資源,因此現在的程序每每容許一個進程包含多個線程,由線程去完成任務。

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

 

 

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

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

 

 

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

 

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

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

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

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

 

 

參考連接John Dalziel, The race for speed part 2: How JavaScript compilers workJake Archibald,Deep dive into the murky waters of script loadingMozilla Developer Network, window.setTimeoutRemy Sharp, Throttling function callsAyman Farhat, An alternative to Javascript’s evil setIntervalIlya Grigorik, Script-injected 「async scripts」 considered harmfulAxel Rauschmayer, ECMAScript 6 promises (1/2): foundationsDaniel Imms, async vs defer attributesCraig Buckler, Load Non-blocking JavaScript with HTML5 Async and DeferDomenico De Felice, How browsers work爲這個值。

相關文章
相關標籤/搜索