W3C在80年代後期90年代初期發明了世界上第一個瀏覽器WorldWideWeb(後改名爲Nexus),支持文本/簡單的樣式表/電影/聲音和圖片
1993年,網景(netscape)瀏覽器誕生,沒有JavaScript,沒有css,只顯示簡單的html元素
1995年,微軟推出聞名世界的IE瀏覽器,自此第一次瀏覽器大戰打響,IE受益於Windows系統得到空前的成功,逐漸取代網景瀏覽器
1998年處於低谷的網景成立了Mozilla基金會,在該基金會推進下,開發了著名的開源項目Firefox並在2004年發佈1.0版本,拉開了第二次瀏覽器大戰的序幕,IE發展更新較緩慢,Firefox一推出就深受你們的喜好,市場份額一直上升。
在Firefox瀏覽器發佈1.0版本的前一年,2003年,蘋果發佈了Safari瀏覽器,並在2005年釋放了瀏覽器中一種很是重要部件的源代碼,發起了一個新的開源項目WebKit
2008年,Google以蘋果開源項目WebKit做爲內核,建立了一個新的項目Chromium,在Chiromium的基礎上,Google發佈了Chromejavascript
上圖是WebKit模塊和其依賴模塊的關係。
在操做系統之上的是WebKit賴以工做的衆多第三方庫,如何高效使用它們是WebKit和各大瀏覽器廠商的一個重大課題。
WebCore部分都是加載和渲染的基礎部分
WebKit Ports是WebKit非共享部分,對於不一樣瀏覽器移植中因爲平臺差別/依賴的第三方庫和需求不一樣等方面緣由,每每按照本身的方式來設計和實現。
在WebCore/js引擎/WebKitPorts之上主要是提供嵌入式編程接口,提供給瀏覽器調用。css
如上圖所示,圖中虛線是與底層第三方庫交互。
當訪問一個頁面的時候,會利用網絡去請求獲取內容,若是命中緩存了,則會在存儲上直接獲取;
若是內容是個HTML格式,首先會找到html解釋器進行解析生成DOM樹,解析到style的時候會找到css解釋器工做獲得CSSOM,解析到script會中止解析並開始解析執行js腳本;
DOM樹和CSSOM樹會構建成一個render樹,render樹上的節點不和DOM樹一一對應,只有顯示節點纔會存在render樹上;
render樹已經知道怎麼繪製了,進入佈局和繪圖,繪製完成後將調用繪製接口,從而顯示在屏幕上。html
上面咱們簡單介紹了整個過程,如今咱們開始認識一下各個步驟的具體過程。前端
字節流通過解碼後是字符流,而後經過詞法分析器會被解釋成詞語(Tokens),以後通過語法分析器構建成節點,最後這些節點組成一棵DOM樹html5
詞法分析
在進行詞法分析前,解釋器首先要檢查網頁內容實用的編碼格式,找到合適的解碼器,將字節流轉換成特定格式的字符串,而後交給詞法分析器進行分析。
每次詞法分析器都會根據上次設置的內部狀態和上次處理後的字符串來生成一個新的詞語。內部使用了超過70種狀態。(ps:生成的詞語還會進過XssAutitor驗證詞語是否安全合法,非合法的詞語不能經過)java
如上圖所示,舉個例子node
<div> <img src="/a" /> </div>
1 接收到"<"進入TagOpen狀態
2 接收"d",根據當前狀態是TagOpen判斷進入TagName狀態,以後接收"i"/"v"
3 接收">"進入TagEnd狀態,此時獲得了div的開始標籤(StartTag)
4 接收"<"進入TagOpen,接收"img"後接收到空格獲得了img開始標籤
6 進入attribute一系列(筆者本身命名的,不知道叫啥)狀態,獲得了src屬性嗎和"/a"屬性值
6 一樣方式得到div結束標籤c++
組成DOM樹
由於節點均可以當作有開始和結束標記,因此用棧的結構來輔助構建DOM樹再合適不過了。
再拿上述的html代碼作例子。web
<div> <img src="/a" /> <span>webkit xiha</span> </div>
1 遇到div開始標籤,將div推入棧;
2 遇到img開始標籤,將img推入棧;
3 遇到src屬性,將src推入棧;
4 將src從棧中取出,做爲DOM樹的一部分;
5 遇到img結束標籤,說明img包裹着src屬性,取出img,做爲src的父親節點;
6 遇到span開始標籤,將span推入棧;
7 遇到文本webkit,將文本推入棧;
8 取出webkit文本,待分發;
9 遇到span結束標籤,說明span標籤包裹着webkit文案,取出span標籤,做爲文本webkit的父親節點;
10 遇到div結束標籤,取出div標籤,說明div標籤包裹着img和span,做爲它們的公共父親節點
看了winter的《重學前端》中《一個瀏覽器是如何工做的》篇章,推翻了上述說法的過程,DOM樹的構建不是從葉子到根的構建過程(自底向上),而是從根到葉子的構建過程(自頂向下)。
1 接收到div開始標籤,將div推入棧,而且在DOM樹上添加div節點,棧頂的節點表示了當前的父節點;
2 接收img開始標籤,將img推入棧,根據棧中前一個節點,img是div的子節點,在DOM樹上:在div節點下添加img節點
3 接收src屬性,非獨立節點,直接添加到img節點下;
4 接收img結束標籤,將棧中img開始標籤退棧;
5 接收span開始標籤,將span推入棧,根據棧中前一個節點,span是div的子節點,在DOM樹上:在div節點下添加span節點;
6 接收文本節點webkit,推入棧,在DOM樹上,在span節點下添加「webkit」文本節點;
7 接收文本節點xiha,根據以前棧頂節點依舊是文本節點,直接將該文本節點合併到前面的文本節點「webkit xiha」;
8 接收span結束標籤,一直執行退棧操做直到將span開始標籤也離開了;
9 接收div結束標籤,退棧;
10 接收endOfFile,DOM樹構建結束;
該棧是HTMLElementStack,該棧的主要做用就是幫助DOM樹維護當前的父節點是哪個(棧頂這個),而且合併能夠合併的詞語。算法
WebKit 使用 Flex 和 Bison 解析器生成器,經過 CSS 語法文件自動建立解析器。最後WebKit將建立好的結果直接設置到StyleSheetContents對象中。
至此咱們已經瞭解到了文檔的解析過程,這裏有一些實驗能夠幫助你更好的瞭解頁面加載過程發生了什麼。聊聊瀏覽器的渲染機制——若邪Y
只要發生樣式的改變,都會觸發檢查是否須要佈局計算
當首次加載頁面/renderStyle改變/滾動操做的時候,都會觸發佈局
佈局是比較耗時的操做,更糟糕的時候佈局的下一步就是渲染,咱們能夠經過硬件加速來跳過佈局和渲染,下面咱們會講到。
一個好的程序經常被劃分爲幾個相互獨立又彼此配合的模塊,瀏覽器也是如此,以 Chrome 爲例,它由多個進程組成,每一個進程都有本身核心的職責,它們相互配合完成瀏覽器的總體功能,每一個進程中又包含多個線程,一個進程內的多個線程也會協同工做,配合完成所在進程的職責。
Chrome 採用多進程架構,其頂層存在一個 Browser process 用以協調瀏覽器的其它進程。
具體說來,Chrome 的主要進程及其職責以下:
Browser Process:
負責包括地址欄,書籤欄,前進後退按鈕等部分的工做; 負責處理瀏覽器的一些不可見的底層操做,好比網絡請求和文件訪問;
Renderer Process:
負責一個 tab 內關於網頁呈現的全部事情
Plugin Process:
負責控制一個網頁用到的全部插件,如 flash
GPU Process
負責處理 GPU 相關的任務
經過「頁面右上角的三個點點點 --- 更多工具 --- 任務管理器」便可打開相關面板
處理輸入
UI thread 須要判斷用戶輸入的是 URL 仍是 query;
開始導航
當用戶點擊回車鍵,UI thread 通知 network thread 獲取網頁內容,並控制 tab 上的 spinner 展示,表示正在加載中。
讀取響應
當請求響應返回的時候,network thread 會依據 Content-Type 及 MIME Type sniffing 判斷響應內容的格式
若是響應內容的格式是 HTML ,下一步將會把這些數據傳遞給 renderer process,若是是 zip 文件或者其它文件,會把相關數據傳輸給下載管理器。
查找渲染進程
當上述全部檢查完成,network thread 確信瀏覽器能夠導航到請求網頁,network thread 會通知 UI thread 數據已經準備好,UI thread 會查找到一個 renderer process 進行網頁的渲染。
確認導航
進過了上述過程,數據以及渲染進程均可用了, Browser Process 會給 renderer process 發送 IPC 消息來確認導航,一旦 Browser Process 收到 renderer process 的渲染確認消息,導航過程結束,頁面加載過程開始。
此時,地址欄會更新,展現出新頁面的網頁信息。history tab 會更新,可經過返回鍵返回導航來的頁面,爲了讓關閉 tab 或者窗口後便於恢復,這些信息會存放在硬盤中。
DOM樹構建完成以後,Webkit還要爲DOM樹構建RenderObject樹。
什麼狀況下會爲一個DOM節點創建新的RenderObject對象呢
1.ducument節點 2.可視節點,例如html,body,div等。而WebKit不會爲非可視化節點建立RenderObject節點,例如link,head,script 3.某些狀況下WebKit會創建匿名的RenderObject,該RenderObject不對應DOM樹的任何節點,例如匿名的RenderBlock tip: 若是一個節點即包含塊級節點又包含內聯節點,會爲內聯節點建立一個RenderBlock,即造成 RenderObject——RenderObject ——RenderBlock——RenderObject
網頁是能夠分層的,可讓WebKit在渲染處理上得到便利。
會產生RenderLayer的狀況:
RenderObject對象知道如何繪製本身了,須要調用繪圖上下文來進行繪圖操做。
渲染方式:軟件渲染(Cpu完成)和硬件加速渲染(Gpu完成)
Renderer進程消息循環調用判斷是否須要從新計算的佈局和更新,如要
Renderer進程建立共享內存
WebKit計算重繪區域中重疊的RenderObject,RenderObject從新繪製,繪製結果到共享內存的位圖中
繪製完成後,Renderer進程發生消息給Browser進程,Browser進程將更新的區域將共享內存的內容繪製到本身對應存儲區域中(繪製過程不會影響該網頁結果的顯示)
Browser進程回覆消息給Renderer,回收共享內存
Browser進程繪製到窗口
GPU硬件進行繪圖和合成,每一個網頁的Renderer進程都是將以前介紹的3D繪圖和合成操做傳遞給GPU進程,由它來統一調度 和執行,在移動端中,GPU進程並不存在,WebKit將全部工做放在Browser進程中的一個線程完成。
GPU進程處理一些命令後,會向Renderer進程報告本身當前的狀態,Renderer進程經過檢查狀態信息和本身的指望結果來肯定是否知足本身的條件。GPU進程最終繪製的結果再也不像軟件渲染那樣經過共享內存的方式傳遞給Browser進程,而是直接將頁面的內容繪製在瀏覽器的標籤窗口
理想狀況,每個層都會有個存儲區域,保存繪圖結果,最後將這些層的內容合併(compositing)
軟件渲染機制是沒有合成階段的,軟件渲染的結果是一個位圖(bitmap),繪製每一層的時候都使用該位圖,區別在於繪製的位置可能不同,每一層按照從後前的順序。這樣軟件繪圖使用的只是一塊內存空間便可。
軟件渲染只能處理2D方面的操做,而且在高fps的繪圖中性能很差,好比視頻和canvas2d等,可是cpu使用的緩存機制有效減小了重複繪製的開銷
硬件繪製和全部的層的合成都使用Gpu完成,硬件加速渲染能支持如今全部的html5定義的2d和3d繪圖標準;另外,因爲軟件渲染沒有爲每一層提供後端存儲,於是須要將和某區域有重疊部分的全部層次相關區域從新繪製一次,而硬件加速渲染只需從新繪製更新發生的層次。
<div id="box"></div> <div id="bo2"></div>
#box { position: relative; width: 100px; height: 100px; background: #ccc; transform: translate3d(0,0,0); transition: transform 2s linear; } #box.move { transform: translate3d(100px,0,0) !important } #box2 { position: relative; width: 100px; height: 100px; background: #ccc; left: 0; transition: left 2s linear; } #box2.move { left: 100px !important }
var box2 = document.getElementById('box2') setTimeout(() => { box2.classList.add('move') }, 200);
首先咱們看下利用開發者工具Layers能夠看到,以下圖,box1利用了transform3d,從而判斷須要爲box1獨立一層,而其餘的內容則依舊附在document層。
咱們切換到performance進行錄製,查看event log以下圖。發如今box2在移動的時候,不斷重複5各過程:
recalculate style——layout——update layer tree——paint——composite layers
也就是說document層不斷得從新計算佈局,從新渲染,再和box2合併layers,這形成了巨大的浪費。咱們接下來來看一些box1的移動。
var box = document.getElementById('box1') setTimeout(() => { box.classList.add('move') }, 200);
以下圖,在box1移動的時候,沒有了佈局和繪製的過程,利用CSS3D加速,只須要在合併層以前改變屬性,再次合併層就能夠了,不須要從新佈局,也沒有繪製步驟,這就是爲何咱們在寫動畫的時候要時候3d啓用硬件加速的緣由,大大減小了佈局繪製的資源浪費。
上面咱們已經把渲染過程瞭解清楚了,接下來來看一下V8引擎這個重頭戲吧~!
當用戶在屏幕上觸發諸如 touch 等手勢時,首先收到手勢信息的是 Browser process, 不過 Browser process 只會感知到在哪裏發生了手勢,對 tab 內內容的處理是仍是由渲染進程控制的。
事件發生時,瀏覽器進程會發送事件類型及相應的座標給渲染進程,渲染進程隨後找到事件對象,交給js引擎處理,若是js代碼中利用了僑界接口將該節點綁定了事件監聽,那麼就會觸發該事件監聽函數。
編譯型語言如c/c++,處理該語言實際上使用編譯器直接將它們編譯成本地代碼,用戶只是使用這些編譯好的本地代碼,被系統的加載起加載執行,這些本地代碼由操做系統調度CPU直接執行
java作法是明顯的兩個階段,首先是編譯,不像c/c++編譯成本地代碼,而是編譯生成字節碼,字節碼是跨平臺的中間表示,而後java虛擬機加載字節碼,使用解釋器執行這些代碼。
V8以前的版本直接的將抽象語法樹經過JIT技術轉換成本地代碼,放棄了在字節碼階段能夠進行的一些性能優化,但保證了執行速度。在V8生成本地代碼後,也會經過Profiler採集一些信息,來優化本地代碼。雖然,少了生成字節碼這一階段的性能優化,但極大減小了轉換時間。
可是在2017年4月底,v8 的 5.9 版本發佈了,新增了一個 Ignition 字節碼解釋器,將默認啓動
故事得從 Chrome 的一個 bug 提及: http://crbug.com/593477 。Bug 的報告人發現,當在 Chrome 51 (canary) 瀏覽器下加載、退出、從新加載 facebook 屢次,並打開 about:tracing 裏的各項監控開關,能夠發現第一次加載時 v8.CompileScript 花費了 165 ms,再次加載加入 V8.ParseLazy 竟然依然花費了 376 ms。按說若是 Facebook 網站的 js 腳本沒有變,Chrome 的緩存功能應該緩存了對 js 腳本的解析結果,不應花費這麼久。這是爲何呢?
這就是以前 v8 將 JS 代碼編譯成機器碼所帶來的問題。由於機器碼佔空間很大,v8 沒有辦法把 Facebook 的全部 js 代碼編譯成機器碼緩存下來,由於這樣不只緩存佔用的內存、磁盤空間很大,並且再次進入時序列化、反序列化緩存所花費的時間也很長,時間、空間成本都接受不了。
在啓動速度方面,現在內存佔用過大的問題消除了,就能夠提早編譯全部代碼了。由於前端工程爲了節省網絡流量,其最終 JS 產品每每不會分發無用的代碼,因此能夠指望所有提早編譯 JS 代碼不會由於編譯了過多代碼而浪費資源。v8 對於 Facebook 這樣的網站就能夠選擇所有提早編譯 JS 代碼到字節碼,並把字節碼緩存下來,如此 Facebook 第二次打開的時候啓動速度就變快了。下圖是舊的 v8 的執行時間的統計數據,其中 33% 的解析、編譯 JS 腳本的時間在新架構中就能夠被縮短。
v8 自身的重構方面,有了字節碼,v8 能夠朝着簡化的架構方向發展,消除 Cranshaft 這個舊的編譯器,並讓新的 Turbofan 直接從字節碼來優化代碼,並當須要進行反優化的時候直接反優化到字節碼,而不須要再考慮 JS 源代碼。最終達到以下圖所示的架構。
其實,Ignition + TurboFan 的組合,就是字節碼解釋器 + JIT 編譯器的黃金組合。這一黃金組合在不少 JS 引擎中都有所使用,例如微軟的 Chakra,它首先解釋執行字節碼,而後觀察執行狀況,若是發現熱點代碼,那麼後臺的 JIT 就把字節碼編譯成高效代碼,以後便只執行高效代碼而再也不解釋執行字節碼。
在V8中創建類有兩個主要的理由,即(1)將屬性名稱相同的對象歸類,及(2)識別屬性名稱不一樣的對象。同一類中的對象有徹底相同的對象描述,而這能夠加速屬性存取。
在V8,符合歸類條件的類會配置在各類JavaScript對象上。對象引用所配置的類。然而這些類只存在於V8做爲方便之用,因此它們是「隱藏」的。
若是對象的描述是相同的,那麼隱藏類也會相同。以下圖的例子中,對象p和q都屬於相同的隱藏類。
咱們隨時能夠在JavaScript中新增或刪除屬性。然而當此事發生時會毀壞歸類條件(概括名稱相同的屬性)。V8藉由創建屬性變化所需的新類來解決。屬性改變的對象透過一個稱爲「類型轉換(class transition)」的程序歸入新級別中。
在類中儲存類變換信息當在對象p中加入新屬性z時,V8會在Point類內的表格上記錄「加入屬性z,創建類Point2」。
當同一Point類的對象q加入屬性z時,V8會先搜尋Point類表。若是它發現了Point2類已加入屬性z時,就會將對象q設定在Point2類。
因此,當咱們不斷利用js弱類型的特色改變某屬性的類型,好比a.x=1;a.x='hello';除了讓一塊兒開發的同事摸不着頭腦這個屬性到底是什麼玩意,還將會不斷破壞隱藏類,帶來必定的性能損壞。因此,在平常開發中,因避免改變變量或者對象屬性的類型。引入TS吧!是時候擁抱TS大法了哈哈哈。
正常訪問對象屬性的過程是:首先獲取隱藏類的地址,而後根據屬性名查找偏移值,而後計算該屬性的地址。雖然相比以往在整個執行環境中查找減少了很大的工做量,但依然比較耗時。能不能將以前查詢的結果緩存起來,供再次訪問呢?固然是可行的,這就是內嵌緩存。
內嵌緩存的大體思路就是將初次查找的隱藏類和偏移值保存起來,當下次查找的時候,先比較當前對象是不是以前的隱藏類,若是是的話,直接使用以前的緩存結果,減小再次查找表的時間。固然,若是一個對象有多個屬性,那麼緩存失誤的機率就會提升,由於某個屬性的類型變化以後,對象的隱藏類也會變化,就與以前的緩存不一致,須要從新使用之前的方式查找哈希表。
V8的垃圾回收策略基於分代回收機制,該機制又基於 世代假說。該假說有兩個特色:
在V8中,將內存分爲了新生代(new space)和老生代(old space)。它們特色以下:
新生代:對象的存活時間較短。新生對象或只通過一次垃圾回收的對象。
老生代:對象存活時間較長。經歷過一次或屢次垃圾回收的對象。
新生代中的對象主要經過 Scavenge 算法進行垃圾回收。Scavenge 的具體實現,主要採用了Cheney算法。
Cheney算法採用複製的方式進行垃圾回收。它將堆內存一分爲二,每一部分空間稱爲 semispace。這兩個空間,只有一個
空間處於使用中,另外一個則處於閒置。使用中的 semispace 稱爲 「From 空間」,閒置的 semispace 稱爲 「To 空間」。
過程以下:
從 From 空間分配對象,若 semispace 被分配滿,則執行 Scavenge 算法進行垃圾回收。
檢查 From 空間的存活對象,若對象存活,則檢查對象是否符合晉升條件,若符合條件則晉升到老生代,不然將對象從 From 空間複製到 To 空間。
若對象不存活,則釋放不存活對象的空間。
完成複製後,將 From 空間與 To 空間進行角色翻轉(flip)。
Scavenge 算法的缺點是,它的算法機制決定了只能利用一半的內存空間。可是新生代中的對象生存週期短、存活對象少,進行對象複製的成本不是很高,於是很是適合這種場景。
Mark-Compact則是將存活的對象移動到一邊,而後再清理端邊界外的內存。
這篇文章參考了大量的文章和書籍,並做出了本身的理解,若有不對之處,請大神指出~
這篇文章我整理了很久,但願轉載代表出處~
參考:
《WebKit技術內幕》——朱永盛
極客《重學前端》——winter
圖解瀏覽器的基本工做原理
深刻理解V8的垃圾回收原理
爲何V8引擎這麼快?
V8引擎詳解
V8 Ignition:JS 引擎與字節碼的不解之緣