大話Chrome瀏覽器原理

一、一個頁面爲什麼4個進程?

(1)主要原因

  • 進程中的任何一個線程崩潰都會導致整個進程崩潰
  • 線程之間的數據時共享的,多頁面使用多線程有安全性問題。
  • 當一個進程關閉後資源的回收時候操作系統控制的,不易出現內存泄漏
  • 插件的崩潰會導致Chrome的不穩定。
  • 所有模塊都在一個進程導致Chrome不流暢

(2)目前Chrome的進程架構

  • 瀏覽器進程:主要負責用戶界面顯示、交互、子進程管理、存儲。
  • 渲染進程:使用Blink排版引擎和V8引擎渲染出頁面,Chrome會爲每一個Tab創建一個渲染進程,每個進程運行在沙箱中。
  • GPU進程:初衷是實現CSS 3D、網頁繪製和Chrome的UI部分。
  • 網絡進程:加載網絡資源。
  • 插件進程:負責插件的運行。

(3)當前的Chrome架構帶來的問題

  • 消耗資源
  • 體系複雜

(4)未來面向服務的架構(SOA)

構建一個更加內聚、鬆耦合、易於維護和擴展的系統

大話Chrome瀏覽器原理

Chrome正在構建操作系統化的Chrome基礎服務,在性能強大的設備上使用多進程的方式運行基礎服務,當在硬件資源受限的設備上使用Chrome的時候,就使用單進程的方式。

二、TCP協議如何保證頁面傳送到瀏覽器?

TCP和UDP協議的傳輸過程思考,引出QUIC和HTTP3。

三、爲什麼第二次打開站點會很快?

瀏覽器發起HTTP請求的流程:

  • 構建請求,首先是構建請求行。
  • 查找緩存,查找瀏覽器緩存失敗纔會進行網絡請求。
  • 通過DNS準備IP地址和端口。
  • 等待TCP隊列,Chrome機制是同一個域名下最多隻能建立6個連接,否則就會進入等待TCP隊列。
  • 建立TCP連接。
  • 發送HTTP請求。
    • 首先發送請求行,分別是請求方法、請求URI、HTTP協議版本。
    • 如果是POST請求,那麼還要發送請求體。
  • 然後服務器返回數據,包括響應頭和響應體。
  • 通常情況下會斷開TCP連接,如果在HTTP頭部加入Connection:Keep-Alive,這樣TCP就不會斷開,連接可以被複用。

四、從輸入URL到頁面展示,中間發生了什麼?

  • 用戶向瀏覽器輸入URL,然後瀏覽器處理用戶輸入,判斷是合法的URL地址還是搜索關鍵字,如果是關鍵字那麼使用默認的搜索引擎構建搜索URL。
  • 檢查是否有緩存內容,否則瀏覽器通過進程間的IPC通信向網絡進程發送請求信息。
  • 網絡進程接收服務器返回的數據,根據狀態碼判斷是否進行重定向等操作。如果是301或302,那麼表示需要重定向,此時網絡進程會在請求頭的Location字段中讀取重定向的地址,再次發送網絡請求。
  • 通過響應頭中的Content-Type判斷進行何種操作。如果是下載類型,那麼網絡進程會把任務交給下載任務管理器。
  • 由於服務器響應數據的時候就已經準備好了渲染進程,那麼會將數據提交給渲染進程。Chrome會爲每個站點打開一個渲染進程。

五、JavaScript、HTML和CSS如何變成頁面?

由於渲染機制的複雜性,所以劃分爲許多子階段,每個子階段都有輸入、輸出和處理過程,這許多子階段構成了渲染流水線

(1)構建DOM

將HTML標籤轉化爲DOM樹,每個節點對應一個HTML標籤。可以通過如下代碼從Chrome開發者工具中獲取當前頁面的DOM樹:

document

HTML解析器會隨着文檔的加載,邊加載邊解析。HTML解析器維護了一個Token棧結構用於計算父子節點之間的關係。

當解析到JavaScript的時候,DOM解析將會停止,執行代碼,因爲JavaScript有可能修改DOM結構。

Chrome在解析之前會有預解析線程先下載文檔內嵌入的JavaScript下載鏈接。

在解析JavaScript之前首先要解析CSS文件,CSS文件加載會阻塞JavaScript腳本執行。

(2)樣式計算

  • 解析CSS文件。當渲染引擎接收到一個CSS文件的時候,會將CSS文本轉換爲樣式表結構。可以通過如下代碼在Chrome開發者工具中獲取當前頁面的樣式表:
document.styleSheets
  • 轉換CSS屬性值,使其標準化。比如2em會被轉換爲35px,HTML顏色會被轉化RGB顏色。
  • 計算出DOM樹每個節點的樣式。CSS具有繼承和層疊規則。這些會在Chrome的Computed標籤中顯示。

(3)佈局節點

  • 創建佈局樹。由於HTML中還包含了許多不可見的元素,因此還需要創建一棵只包含可見元素的佈局樹。
  • 然後將可見佈局樹和Computed CSS合成帶有CSS的DOM樹。

(4)圖層樹

渲染引擎還需要爲特定的節點生成專門的圖層,並生成一棵圖層樹。可以在Chrome的Layers標籤中查看。

並不是每一個節點都會對應一個圖層,如果一個節點沒有圖層,那麼這個節點就屬於父節點的圖層。

  • 擁有層疊上下文屬性的HTML元素會提升爲一個圖層。
  • 需要進行裁剪的HTML元素,比如overflow屬性。

(5)圖層繪製

渲染引擎會將圖層樹中的每個圖層進行繪製,首先會將每一層的繪製拆分成許多繪製指令,然後繪製指令按照順序組成待繪製列表

(6)柵格化操作

  • 主線程將待繪製列表準備好後提交給合成線程。通常情況下,一個頁面可能很大,但是視口ViewPort是有限大的。因此合成線程會將圖層劃分爲圖塊,通常是256*256或者512*512

  • 合成線程會將視口附近的圖塊來優先生成位圖,實際生成位圖的操作由柵格化線程來執行。
  • 柵格化線程通常情況下會使用GPU來完成,也叫快速柵格化
  • GPU生成的位圖保存在GPU的顯存中。瀏覽器中有個viz組件用來接收合成線程的DrawQuad命令,然後瀏覽器根據該命令將頁面內容顯示在屏幕上。

(7)3個重要概念

  • 重排:更新元素的幾何屬性。通過CSS或者JS修改了元素的位置屬性,那麼就會觸發瀏覽器重新佈局,導致需要完整的渲染流水線。
  • 重繪:更新元素的繪製屬性。如果更改了頁面的顏色屬性,那麼就會省去佈局和分層階段。
  • 合成:比如使用了CSS的transform屬性,那麼就會避開重繪和重排。

六、JavaScript是按照順序執行的嗎?

變量提升:JavaScript解析引擎執行代碼過程中,將變量的聲明部分和函數的聲明部分提升到代碼開頭的行爲,變量提升以後會給變量設置默認值,這個默認是就是undefined

變量提升發生在編譯階段,在這個階段會生成執行上下文可執行代碼。在執行上下文中保存了變量環境對象,該對象保存了變量提升的內容。

如果函數或者變量出現了重名,那麼變量環境對象將會發生覆蓋。

console.log(x);
var x = 10;
var x = 20;

f1();
function f1() {console.log('method: f1');}
function f1() {console.log('method: f1 override');}

f2();
var f2 = function() {console.log('method: f2');}
var f2 = function() {console.log('method: f2 override');}
undefined
method: f1 override
/Users/koils/test.js:9
f2();
^

TypeError: f2 is not a functio

七、爲什麼JavaScript會出現棧溢出?

在JavaScript中每個函數都有自己的執行上下文,JavaScript使用調用棧來管理這些執行上下文環境。全局執行上下文位於棧底。

調用棧是JavaScript引擎追蹤函數執行的一個機制。

棧溢出:棧是有大小的,當入棧數目超過這個大小就會造成棧溢出現象。

八、作用域、作用域鏈和閉包

ES6中通過引入塊級作用域配合let和const來避免變量提升這個設計缺陷。

作用域:是指在程序中定義變量的區域,這個位置決定了變量的生命週期。作用域是變量和函數的可訪問範圍。在ES6之前只有全局作用域函數作用域,之後支持塊級作用域。

變量提升帶來的問題:

  • 變量容易在不被察覺的情況下被覆蓋掉。
  • 本來應該銷燬的變量沒有被銷燬

JavaScript如何支持塊級作用域?通過let聲明的變量在編譯階段會被存放到詞法環境,因此是使用詞法環境來支持的。

在JavaScript的每個執行上下文中都包含一個叫做outer的外部引用,用來指向外部的執行上下文。當進行變量查找的時候在當前作用域找不到就回去outer中查找,直到找到。這個查找鏈條叫做作用域鏈

詞法作用域:作用域由代碼中的函數聲明的位置來決定,是靜態的作用域,通過這個作用域可以預測代碼的執行過程。詞法作用域在代碼階段就決定好了,與函數如何調用無關。

閉包:在JavaScript中,根據詞法作用域的規則,內部函數總是可以訪問其外部函數聲明的變量,當通過調用一個外部函數返回一個內部函數的時候,即使該外部函數已經執行結束,但是內部函數引用外部函數變量依然保存在內存中,就把這些變量的集合稱爲閉包。

閉包的回收:如果閉包會一直被使用,那麼可以當做全局變量存在。但是如果使用頻率不高,而且佔用內存較大,儘量讓該閉包作爲局部變量。

九、this

(1)全局執行上下文的this

全局執行上下文的this指向window對象

(2)函數執行上下文的this

默認情況下也是指向window對象。設置this指向的方法有三種:

  • call方法,bind方法和apply方法

  • 通過對象調用位置:使用對象調用其內部的一個方法,該方法的this是指向對象本身的。

    var object = {
    fn: function () {console.log(this);}
    };
    object.fn();
  • 通過構造函數:函數中的this屬於新對象

    function fn () {this.x = 'HelloWorld';}
    var object = new fn();

(3)this的設計缺陷

  • 嵌套函數中this不會從外層函數中繼承,解決方法有

    • 將this體系轉化爲作用域體系
    function fn () {
      this.x = 1;
      var that = this;
      function fx () {
        that.x = 10;
      }
    }
    • 使用ES6中的箭頭函數
    function fn () {
      this.x = 1;
      var fx = () => {this.x = 10;};
    }
  • 普通函數的this指向window對象,這個問題可以通過使用嚴格模式解決。

十、JavaScript的內存機制

(1)數據存儲

  • 原始類型
類型 描述
Boolean 只有true和false兩個值
Null 只有一個值null,使用typeof檢測時會返回object類型,這是JavaScript的Bug
undefined 一個沒有被賦值的默認值,變量提升時也會使用該值
Number 數字類型,64位二進制格式
BigInt 可以用於表示任何精度
String 表示文本數據,不可變
Symbol 唯一且不可修改,通常用於作爲Object和Key
Object 一組屬性的集合
  • 引用類型

JavaScript的內存空間分爲棧空間、堆空間和代碼空間。棧空間用於存儲執行上下文。在JavaScript的賦值過程中,引用類型只會複製內存地址。

(2)垃圾回收

  • 調用棧中的垃圾回收

    JavaScript引擎通過向下移動ESP來銷燬該函數保存在棧中執行的上下文。

  • 堆中的垃圾回收

    • JavaScript使用垃圾回收器收集垃圾。待際假說:大部分對象在內存中存活時間會很短,不死的對象會活的更久。在V8引擎中分爲新生代和老年代,新生代通常是1-8MB的內存空間,並且兩個區域使用不同的GC機制。

    • 新生代使用Scavenge算法,它將新生代劃分爲兩個區域,一半是對象區域,另一半是空閒區域。當對象區域寫滿以後就進行GC,首先對對象區域的對象進行標記,然後再清理垃圾,副垃圾收集器將這些沒有變成垃圾的對象複製到空閒區域,然後有序的排列,最後將對象區域和空閒區域進行角色翻轉
    • JavaScript的主垃圾回收器主要進行老年代的垃圾回收工作,使用標記-清除算法。
    • 當JavaScript的進行GC的時候,會產生StopTheWorld(全停頓)現象。由於老年代受到GC全停頓的影響較大,因此老年代的垃圾回收使用增量-標記算法,使得JavaScript腳本的執行和GC兩個線程交替執行

(3)解釋編譯

在JavaScript的執行引擎V8中,既有解釋器(Ignition)也存在編譯器(TurboFan)。

  • 首先會從JavaScript代碼翻譯爲AST並生成執行上下文。AST是⾮常重要的⼀種數據結構,在很多項⽬中有着⼴泛的應⽤。其中最著名的⼀個項⽬是Babel。的⼯作原理就是先將ES6源碼轉換爲AST,然後再將ES6語法的AST 轉換爲ES5語法的AST,最後利⽤ES5的AST⽣成JavaScript源代碼。
    • 詞法分析,生成Token。
    • 語法分析,解析Token生成AST。
  • 生成字節碼。解釋器根據AST解釋並執行字節碼。字節碼是介於AST和機器碼之間的一種代碼,與特定類型的機器無關。
  • 執行代碼。多次重複執行的代碼會選定爲熱點代碼,由編譯器編譯爲機器代碼並保存。解釋器Ignition在解釋執⾏字節碼 的同時,收集代碼信息,當它發現某⼀部分代碼變熱了之後,TurboFan編譯器把熱點的字節 碼轉換爲機器碼,並把轉換後的機器碼保存起來,以備下次使⽤,這叫做JIT即時編譯

十一、消息隊列和事件循環

  • Chrome將事件存放到隊列,然後使用循環機制將消息取出,然後執行。比如渲染進程專門有一個IO線程用於通過隊列接受其他線程傳來的任務。
  • 當線程需要安全的退出的時候,由於在進程中設置了退出標誌,每次在隊列中取出任務執行之前都需要檢查標誌。
  • 對於高優先級任務的處理,比如監聽DOM節點的變化情況,會作爲微任務添加到隊列中宏任務微任務隊列中,當任務執行完成後檢查當前任務的微任務隊列是否存在微任務,有就取出來執行。
  • 通過PromiseMutationObserver監控某個DOM節點都會產生微任務。

十二、JavaScript面向對象

(1)封裝

由於JavaScript沒有提供權限訪問修飾符,因此可以通過閉包的方式實現私有變量的保護

function Book(name) {
  this.getName = () => {return name;}
  this.setName = (x) => {name = x;}
}

let book = new Book("HelloWorld");
book.setName("JavaScript");
book.getName();

(2)繼承

在ES6之前,沒有extends關鍵字,最常見的叫做原型鏈繼承。原型prototype是JavaScript函數中的一個內置屬性,指向另外一個對象,被指向的對象的所有的屬性和方法都會被當前的實例所繼承。

  • 設置prototype的代碼需要放到構造器之外。
  • 設置prototype的代碼需要放到任何實例化之前。

原型鏈繼承無法解決父類構造方法存在參數的問題,因此可以通過構造繼承解決:

function Base1(name) {this.name = name;}

function Base2(age) {this.age = age;}

function Child(name, age) {
  Base1.call(this, name);
  Base2.call(this, age);
}

(3)多態

  • 當創建類的實例的時候,沒有使用new關鍵字,this指的是window對象,否則指向的是當前實例對象。
  • 當類存在return語句的時候,如果返回的是基本數據類型,那麼this就會強制指定爲當前類對象;如果返回的是引用數據類型,那麼會遵循return語句。

十三、setTimeout實現原理

Chrome中使用延遲隊列保存Chrome內部的延時任務和setTimeout提交的延時任務。當執行完消息隊列中的任務之後就會開始執行延時隊列的處理函數,然後延時隊列處理函數會根據發起時間和延遲時間計算出到期任務。

使用setTimeout的注意事項:

  • 如果當前任務執行時間過久,會影響到定時器的執行。

  • 如果setTimout存在嵌套,那麼系統會設置4ms的間隔時間。

  • 當前頁面標籤如果沒有被激活,那麼setTimeout的執行最小時間間隔是1s。目的是優化加載消耗和耗電量。

  • 延遲執行時間有最大值,當延時24.8天setTimeout就會溢出,因爲setTimeout使用的是32bit來存儲。

  • setTimeout執行的函數this對象指向window,可以通過匿名函數或者bind方法解決:

    setTimeout(function() {}, 1);
    setTimeout(() => {}, 1);
    
    setTimeout(object.func.bind(object), 1);

十四、瀏覽器緩存

(1)強緩存與協商緩存

在瀏覽器中分爲強緩存和協商緩存。強緩存不需要發送HTTP請求,當檢查是否是強緩存的時候在HTTP1.0和HTTP1.1中是不一樣的:

  • 早期的HTTP1.0使用的是Expires字段,它指明瞭過期時間。
  • HTTP1.1使用的是Cache-Control字段,有以下參數:
    • 通過max-age指明緩存存活時間。
    • private表示只有瀏覽器才能緩存,中間代理服務器無法緩存。
    • no-cache表示跳過強緩存,直接進入協商緩存階段
    • no-store表示直接不進行緩存
    • s-maxage表示針對代理服務器的緩存時長
    • Expires和Cache-Control同時存在的時候,優先考慮Cache-Control

當強緩存失效之後,瀏覽器在請求頭中攜帶相應的緩存tag來向服務器發請求,由服務器根據這個tag,來決定是否使用緩存,這就是協商緩存

  • Last-Modified:即最後修改時間。在瀏覽器第一次給服務器發送請求後,服務器會在響應頭中加上這個字段。瀏覽器接收到後,如果再次請求,會在請求頭中攜帶If-Modified-Since字段,這個字段的值也就是服務器傳來的最後修改時間。服務器拿到請求頭中的If-Modified-Since的字段後,其實會和這個服務器中Last-Modified對比:
    • 如果請求頭中的這個值小於最後修改時間,說明是時候更新了。返回新的資源,跟常規的HTTP請求響應的流程一樣。
    • 否則返回304,告訴瀏覽器直接用緩存。
  • ETag:ETag 是服務器根據當前文件的內容,給文件生成的唯一標識,只要裏面的內容有改動,這個值就會變。服務器通過響應頭把這個值給瀏覽器。瀏覽器接收到ETag的值,會在下次請求時,將這個值作爲If-None-Match這個字段的內容,並放到請求頭中,然後發給服務器。服務器接收到If-None-Match後,會跟服務器上該資源的ETag進行比對:
    • 如果兩者不一樣,說明要更新了。返回新的資源,跟常規的HTTP請求響應的流程一樣。
    • 否則返回304,告訴瀏覽器直接用緩存。

在精準度上,ETag優於Last-Modified。優於 ETag 是按照內容給資源上標識,因此能準確感知資源的變化。Last-Modified 能夠感知的單位時間是秒,如果文件在 1 秒內改變了多次,那麼這時候的 Last-Modified 並沒有體現出修改了。

在性能上,Last-Modified優於ETag,也很簡單理解,Last-Modified僅僅只是記錄一個時間點,而 Etag需要根據文件的具體內容生成哈希值。

(2)Service Worker Cache

Service Worker 借鑑了 Web Worker的 思路,即讓 JS 運行在主線程之外,由於它脫離了瀏覽器的窗體,因此無法直接訪問DOM。雖然如此,但它仍然能幫助我們完成很多有用的功能,比如離線緩存、消息推送和網絡代理等功能。其中的離線緩存就是 Service Worker Cache。Service Worker 同時也是 PWA 的重要實現機制。

(3)Memory Cache

內存緩存,從效率上講它是最快的。但是從存活時間來講又是最短的,當渲染進程結束後,內存緩存也就不存在了。

(4)Disk Cache

存儲在磁盤中的緩存,從存取效率上講是比內存緩存慢的,但是他的優勢在於存儲容量和存儲時長。

(5)Push Cache

即推送緩存,這是瀏覽器緩存的最後一道防線。它是 HTTP/2中的內容,雖然現在應用的並不廣泛,但隨着 HTTP/2 的推廣,它的應用越來越廣泛。

十五、瀏覽器存儲

(1)Cookie

Cookie 本質上就是瀏覽器裏面存儲的一個很小的文本文件,內部以鍵值對的方式來存儲。向同一個域名下發送請求,都會攜帶相同的 Cookie,服務器拿到 Cookie 進行解析,便能拿到客戶端的狀態。

Cookie就是用來做狀態存儲的。缺陷如下:

  • 容量缺陷。Cookie 的體積上限只有4KB。
  • 性能缺陷。Cookie 緊跟域名,不管域名下面的某一個地址需不需要這個 Cookie ,請求都會攜帶上完整的 Cookie,這樣隨着請求數的增多,其實會造成巨大的性能浪費的,因爲請求攜帶了很多不必要的內容。
  • 安全缺陷。由於 Cookie 以純文本的形式在瀏覽器和服務器中傳遞。在HttpOnly爲 false 的情況下,Cookie 信息能直接通過 JS 腳本來讀取。

(2)localStorage

也是針對一個域名,即在同一個域名下,會存儲相同的一段localStorage。與Cookie的區別如下:

  • 容量。localStorage 的容量上限爲5M。對於一個域名是持久存儲的。
  • 只存在客戶端,默認不參與與服務端的通信。這樣就很好地避免了 Cookie 帶來的性能問題和安全問題。
  • 接口封裝。通過localStorage暴露在全局,並通過它的 setItem 和 getItem等方法進行操作。

(3)sessionStorage

  • 容量。容量上限也爲 5M。
  • 只存在客戶端,默認不參與與服務端的通信。
  • 接口封裝。

sessionStoragelocalStorage有一個本質的區別,那就是前者只是會話級別的存儲,並不是持久化存儲。會話結束,也就是頁面關閉,這部分sessionStorage就不復存在了。

(4)IndexedDB

IndexedDB是運行在瀏覽器中的非關係型數據庫, 本質上是數據庫,理論上這個容量是沒有上限的。支持事務和二進制存儲。

  • 鍵值對存儲,內部採用對象倉庫存儲方式。
  • 異步操作,數據庫的讀寫屬於IO操作,瀏覽器提供了異步IO支持。
  • 受到同源策略限制,無法跨域訪問數據庫。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章