在瀏覽器輸入URL回車後發生了什麼?

本文由 簡悅 SimpRead 轉碼, 原文地址 https://4ark.me/post/b6c7c0a2.htmljavascript

這個問題已是老生常談了,更是常常被做爲面試的壓軸題出現,網上也有不少文章,但最近閒的無聊,而後就本身作了一篇筆記,感受比以前理解更透徹了。css

前言

這個問題已是老生常談了,更是常常被做爲面試的壓軸題出現,網上也有不少文章,但最近閒的無聊,而後就本身作了一篇筆記,感受比以前理解更透徹了。html

這篇筆記是我這兩天看了數十篇文章總結出來的,因此相對全面一點,但因爲我是作前端的,因此會比較重點分析瀏覽器渲染頁面那一部分,至於其餘部分我會羅列出關鍵詞,感興趣的能夠自行查閱,前端

注意:本文的步驟是創建在,請求的是一個簡單的 HTTP 請求,沒有 HTTPS、HTTP二、最簡單的 DNS、沒有代理、而且服務器沒有任何問題的基礎上,儘管這是不切實際的。html5

大體流程

  1. URL 解析
  2. DNS 查詢
  3. TCP 鏈接
  4. 處理請求
  5. 接受響應
  6. 渲染頁面

1、URL 解析

地址解析:java

首先判斷你輸入的是一個合法的 URL 仍是一個待搜索的關鍵詞,而且根據你輸入的內容進行自動完成、字符編碼等操做。git

HSTSgithub

因爲安全隱患,會使用 HSTS 強制客戶端使用 HTTPS 訪問頁面。詳見:你所不知道的 HSTS面試

其餘操做瀏覽器

瀏覽器還會進行一些額外的操做,好比安全檢查、訪問限制(以前國產瀏覽器限制 996.icu)。

檢查緩存

2、DNS 查詢

基本步驟

1. 瀏覽器緩存

瀏覽器會先檢查是否在緩存中,沒有則調用系統庫函數進行查詢。

2. 操做系統緩存

操做系統也有本身的 DNS 緩存,但在這以前,會向檢查域名是否存在本地的 Hosts 文件裏,沒有則向 DNS 服務器發送查詢請求。

3. 路由器緩存

路由器也有本身的緩存。

4. ISP DNS 緩存

ISP DNS 就是在客戶端電腦上設置的首選 DNS 服務器,它們在大多數狀況下都會有緩存。

根域名服務器查詢

在前面全部步驟沒有緩存的狀況下,本地 DNS 服務器會將請求轉發到互聯網上的根域,下面這個圖很好的詮釋了整個流程:

根域名服務器:維基百科

須要注意的點

  1. 遞歸方式:一路查下去中間不返回,獲得最終結果才返回信息(瀏覽器到本地 DNS 服務器的過程)
  2. 迭代方式,就是本地 DNS 服務器到根域名服務器查詢的方式。
  3. 什麼是 DNS 劫持
  4. 前端 dns-prefetch 優化

3、TCP 鏈接

TCP/IP 分爲四層,在發送數據時,每層都要對數據進行封裝:

1. 應用層:發送 HTTP 請求

在前面的步驟咱們已經獲得服務器的 IP 地址,瀏覽器會開始構造一個 HTTP 報文,其中包括:

  • 請求報頭(Request Header):請求方法、目標地址、遵循的協議等等
  • 請求主體(其餘參數)

其中須要注意的點:

  • 瀏覽器只能發送 GET、POST 方法,而打開網頁使用的是 GET 方法

2. 傳輸層:TCP 傳輸報文

傳輸層會發起一條到達服務器的 TCP 鏈接,爲了方便傳輸,會對數據進行分割(以報文段爲單位),並標記編號,方便服務器接受時可以準確地還原報文信息。

在創建鏈接前,會先進行 TCP 三次握手。

關於 TCP/IP 三次握手,網上已經有不少段子和圖片生動地描述了。

相關知識點:

  1. SYN 泛洪攻擊

3. 網絡層:IP 協議查詢 Mac 地址

將數據段打包,並加入源及目標的 IP 地址,而且負責尋找傳輸路線。

判斷目標地址是否與當前地址處於同一網絡中,是的話直接根據 Mac 地址發送,不然使用路由表查找下一跳地址,以及使用 ARP 協議查詢它的 Mac 地址。

注意:在 OSI 參考模型中 ARP 協議位於鏈路層,但在 TCP/IP 中,它位於網絡層。

4. 鏈路層:以太網協議

以太網協議

根據以太網協議將數據分爲以 「幀」 爲單位的數據包,每一幀分爲兩個部分:

  • 標頭:數據包的發送者、接受者、數據類型
  • 數據:數據包具體內容

Mac 地址

以太網規定了連入網絡的全部設備都必須具有 「網卡」 接口,數據包都是從一塊網卡傳遞到另外一塊網卡,網卡的地址就是 Mac 地址。每個 Mac 地址都是獨一無二的,具有了一對一的能力。

廣播

發送數據的方法很原始,直接把數據經過 ARP 協議,向本網絡的全部機器發送,接收方根據標頭信息與自身 Mac 地址比較,一致就接受,不然丟棄。

注意:接收方迴應是單播。

相關知識點:

  1. ARP 攻擊

服務器接受請求

接受過程就是把以上步驟逆轉過來,參見上圖。

4、服務器處理請求

大體流程

HTTPD

最多見的 HTTPD 有 Linux 上經常使用的 Apache 和 Nginx,以及 Windows 上的 IIS。

它會監聽獲得的請求,而後開啓一個子進程去處理這個請求。

處理請求

接受 TCP 報文後,會對鏈接進行處理,對 HTTP 協議進行解析(請求方法、域名、路徑等),而且進行一些驗證:

  • 驗證是否配置虛擬主機
  • 驗證虛擬主機是否接受此方法
  • 驗證該用戶可使用該方法(根據 IP 地址、身份信息等)

重定向

假如服務器配置了 HTTP 重定向,就會返回一個 301永久重定向響應,瀏覽器就會根據響應,從新發送 HTTP 請求(從新執行上面的過程)。

關於更多:詳見這篇文章

URL 重寫

而後會查看 URL 重寫規則,若是請求的文件是真實存在的,好比圖片、html、css、js 文件等,則會直接把這個文件返回。

不然服務器會按照規則把請求重寫到 一個 REST 風格的 URL 上。

而後根據動態語言的腳本,來決定調用什麼類型的動態文件解釋器來處理這個請求。

以 PHP 語言的 MVC 框架舉例,它首先會初始化一些環境的參數,根據 URL 由上到下地去匹配路由,而後讓路由所定義的方法去處理請求。

5、瀏覽器接受響應

瀏覽器接收到來自服務器的響應資源後,會對資源進行分析。

首先查看 Response header,根據不一樣狀態碼作不一樣的事(好比上面提到的重定向)。

若是響應資源進行了壓縮(好比 gzip),還須要進行解壓。

而後,對響應資源作緩存。

接下來,根據響應資源裏的 MIME 類型去解析響應內容(好比 HTML、Image 各有不一樣的解析方式)。

6、渲染頁面

瀏覽器內核

不一樣的瀏覽器內核,渲染過程也不徹底相同,但大體流程都差很少。

基本流程

1.HTML 解析

首先要知道瀏覽器解析是從上往下一行一行地解析的。

解析的過程能夠分爲四個步驟:

1. 解碼(encoding)

傳輸回來的其實都是一些二進制字節數據,瀏覽器須要根據文件指定編碼(例如 UTF-8)轉換成字符串,也就是 HTML 代碼。

2. 預解析(pre-parsing)

預解析作的事情是提早加載資源,減小處理時間,它會識別一些會請求資源的屬性,好比img標籤的src屬性,並將這個請求加到請求隊列中。

3. 符號化(Tokenization)

符號化是詞法分析的過程,將輸入解析成符號,HTML 符號包括,開始標籤、結束標籤、屬性名和屬性值。

它經過一個狀態機去識別符號的狀態,好比遇到<>狀態都會產生變化。

4. 構建樹(tree construction)

注意:符號化和構建樹是並行操做的,也就是說只要解析到一個開始標籤,就會建立一個 DOM 節點。

在上一步符號化中,解析器得到這些標記,而後以合適的方法建立DOM對象並把這些符號插入到DOM對象中。

<html>
<head>
    <title>Web page parsing</title>
</head>
<body>
    <div>
        <h1>Web page parsing</h1>
        <p>This is an example Web page.</p>
    </div>
</body>
</html>

瀏覽器容錯進制

你歷來沒有在瀏覽器看過相似」 語法無效」 的錯誤,這是由於瀏覽器去糾正錯誤的語法,而後繼續工做。

事件

當整個解析的過程完成之後,瀏覽器會經過DOMContentLoaded事件來通知DOM解析完成。

2. CSS 解析

一旦瀏覽器下載了 CSS,CSS 解析器就會處理它遇到的任何 CSS,根據語法規範解析出全部的 CSS 並進行標記化,而後咱們獲得一個規則表。

CSS 匹配規則

在匹配一個節點對應的 CSS 規則時,是按照從右到左的順序的,例如:div p { font-size :14px }會先尋找全部的p標籤而後判斷它的父元素是否爲div

因此咱們寫 CSS 時,儘可能用 id 和 class,千萬不要過分層疊。

3. 渲染樹

其實這就是一個 DOM 樹和 CSS 規則樹合併的過程。

注意:渲染樹會忽略那些不須要渲染的節點,好比設置了display:none的節點。

計算

經過計算讓任何尺寸值都減小到三個可能之一:auto、百分比、px,好比把rem轉化爲px

級聯

瀏覽器須要一種方法來肯定哪些樣式才真正須要應用到對應元素,因此它使用一個叫作specificity的公式,這個公式會經過:

  1. 標籤名、class、id
  2. 是否內聯樣式
  3. !important

而後得出一個權重值,取最高的那個。

渲染阻塞

當遇到一個script標籤時,DOM 構建會被暫停,直至腳本完成執行,而後繼續構建 DOM 樹。

但若是 JS 依賴 CSS 樣式,而它尚未被下載和構建時,瀏覽器就會延遲腳本執行,直至 CSS Rules 被構建。

全部咱們知道:

  • CSS 會阻塞 JS 執行
  • JS 會阻塞後面的 DOM 解析

爲了不這種狀況,應該如下原則:

  • CSS 資源排在 JavaScript 資源前面
  • JS 放在 HTML 最底部,也就是 </body>

另外,若是要改變阻塞模式,可使用 defer 與 async,詳見:這篇文章

4. 佈局與繪製

肯定渲染樹種全部節點的幾何屬性,好比:位置、大小等等,最後輸入一個盒子模型,它能精準地捕獲到每一個元素在屏幕內的準確位置與大小。

而後遍歷渲染樹,調用渲染器的 paint() 方法在屏幕上顯示其內容。

5. 合併渲染層

把以上繪製的全部圖片合併,最終輸出一張圖片。

6. 迴流與重繪

迴流 (reflow)

當瀏覽器發現某個部分發現變化影響了佈局時,須要倒回去從新渲染,會從html標籤開始遞歸往下,從新計算位置和大小。

reflow 基本是沒法避免的,由於當你滑動一下鼠標、resize 窗口,頁面就會產生變化。

重繪 (repaint)

改變了某個元素的背景色、文字顏色等等不會影響周圍元素的位置變化時,就會發生重繪。

每次重繪後,瀏覽器還須要合併渲染層並輸出到屏幕上。

迴流的成本要比重繪高不少,因此咱們應該儘可能避免產生迴流。

好比:

  • display:none 會觸發迴流,而 visibility:hidden 只會觸發重繪。

7. JavaScript 編譯執行

大體流程

能夠分爲三個階段:

1. 詞法分析

JS 腳本加載完畢後,會首先進入語法分析階段,它首先會分析代碼塊的語法是否正確,不正確則拋出 「語法錯誤」,中止執行。

幾個步驟:

  • 分詞,例如將var a = 2,,分紅vara=2這樣的詞法單元。
  • 解析,將詞法單元轉換成抽象語法樹(AST)。
  • 代碼生成,將抽象語法樹轉換成機器指令。

2. 預編譯

JS 有三種運行環境:

  • 全局環境
  • 函數環境
  • eval

每進入一個不一樣的運行環境都會建立一個對應的執行上下文,根據不一樣的上下文環境,造成一個函數調用棧,棧底永遠是全局執行上下文,棧頂則永遠是當前執行上下文。

建立執行上下文

建立執行上下文的過程當中,主要作了如下三件事:

  • 建立變量對象
    • 參數、函數、變量
  • 創建做用域鏈
    • 確認當前執行環境是否能訪問變量
  • 肯定 This 指向

3. 執行

JS 線程

雖然 JS 是單線程的,但實際上參與工做的線程一共有四個:

其中三個只是協助,只有 JS 引擎線程是真正執行的

  • JS 引擎線程:也叫 JS 內核,負責解析執行 JS 腳本程序的主線程,例如 V8 引擎
  • 事件觸發線程:屬於瀏覽器內核線程,主要用於控制事件,例如鼠標、鍵盤等,當事件被觸發時,就會把事件的處理函數推動事件隊列,等待 JS 引擎線程執行
  • 定時器觸發線程:主要控制setIntervalsetTimeout,用來計時,計時完畢後,則把定時器的處理函數推動事件隊列中,等待 JS 引擎線程。
  • HTTP 異步請求線程:經過 XMLHttpRequest 鏈接後,經過瀏覽器新開的一個線程,監控 readyState 狀態變動時,若是設置了該狀態的回調函數,則將該狀態的處理函數推動事件隊列中,等待 JS 引擎線程執行。

注:瀏覽器對同一域名的併發鏈接數是有限的,一般爲 6 個。

宏任務

分爲:

  • 同步任務:按照順序執行,只有前一個任務完成後,才能執行後一個任務
  • 異步任務:不直接執行,只有知足觸發條件時,相關的線程將該異步任務推動任務隊列中,等待 JS 引擎主線程上的任務執行完畢時纔開始執行,例如異步 Ajax、DOM 事件,setTimeout 等。

微任務

微任務是 ES6 和 Node 環境下的,主要 API 有:Promiseprocess.nextTick

微任務的執行在宏任務的同步任務以後,在異步任務以前。

代碼例子

console.log('1'); // 宏任務 同步

setTimeout(function() {
    console.log('2'); // 宏任務 異步
})

new Promise(function(resolve) {
    console.log('3'); // 宏任務 同步
    resolve();
}).then(function() {
    console.log('4') // 微任務
})

console.log('5') // 宏任務 同步

以上代碼輸出順序爲:1,3,5,4,2

參考文檔

相關文章
相關標籤/搜索