當咱們去探索瀏覽器運行原理的時候,咱們傾向於執行多個例子去推斷其內在的設計;在很長一段時間內,我也是這麼去探索瀏覽器這個黑盒的,但這麼作始終只是驗證例子自己而不能斷言瀏覽器實際的行爲。爲了追求真理(誤),決定寫這個系列的文章,從源頭探索瀏覽器的行爲準則。javascript
HTML Standard即由w3c制定的html規範,而咱們實際使用的瀏覽器的核心,諸如chrome內部的webkit,則是html規範的實現。全部界面行爲(不包括瀏覽器自身元件如書籤)都由HTML規範所描述,並由webkit(或者其餘瀏覽器引擎)實現。須要注意的是Javascript規範並非由w3c制定的,HTML規範只定義了其中的Document和window對象,也就是常說的DOM和BOM,咱們接下來探究的就是HTML的規範是如何定義的。css
須要注意一些點:html
因爲上述緣由,在非webkit內核瀏覽器測試本文用例可能會有問題。前端
準確的說瀏覽器不僅能解析HTML,還能解析包括XML文檔、大部分圖片格式、PDF文件等等,可是咱們如今只關注HTML是如何被解析的。java
在開始探討瀏覽器解析html的流程以前,先對Dom進行一下定義:node
本文將HTML文本生成Dom的過程稱爲解析(Parser),將Dom Tree + Css生成Layout Tree再進行繪製的過程稱爲渲染(Render)。react
Dom對於瀏覽器而言,就像Virtual Dom於前端開發者,Dom並非真實的視圖(咱們所看到的界面),但瀏覽器能夠根據Dom Tree和Css計算出底層繪製指令。git
而HTML Parser產出的是Dom,而不是實際的界面,且Dom的變化並不會馬上致使繪製(相似react調用setState)。github
因此當HTML Parser阻塞意味着暫停產出新的節點(eg: HTMLElement),有些時候HTML Parser阻塞並不表明瀏覽器沒法繪製界面。(若是已經存在部分Dom和Css我爲何不能繪製?)web
讓咱們一步步揭開瀏覽器的神祕面紗。
當咱們打開一個網頁的時候,實際上瀏覽器發起了一個請求,最終將請求結果呈現給用戶。做爲開發者,咱們關心的是:在這個過程當中瀏覽器應該進行怎麼樣的準備工做?是如何去處理請求的響應的?帶着這兩個疑問,咱們繼續往下看。
那麼瀏覽器是如何作的呢?瀏覽器在請求資源之初會初始化一個獨立的上下文稱爲browsing context包含了:
當請求的資源是一個HTML文件的時候(瀏覽器使用content-type識別),瀏覽器會初始化一個HTML Parser關聯到當前的document,並將響應結果傳入給HTML Parser進行解析,這是HTML Standard中規定的parser流程:
咱們來一步步理解這流程的意義。咱們從file system或者http response中拿到是字節流,拿到字節流後瀏覽器會嘗試去decode,會根據以下的設定選取解碼器:
此外規範還推薦使用兼容ascii編碼的編碼(例如utf-8)(vsc默認使用該編碼保存文檔)去編寫HTML文檔,由於規範使用ascii編碼探測meta標籤,進而獲取文檔的編碼。
一個使用了錯誤編碼解析的文檔:
至此,咱們的瀏覽器終於能正確的將字節流decode了,可是在處理字符構建Dom以前,還須要額外的預處理,稱爲Input Stream Preprocessor,這個步驟執行的作的事情僅僅是標準化換行符,由於在不一樣系統下使用文本使用的換行符是不一致的,例如Windows使用CRLF做爲換行符,而類Unix系統使用LF做爲換行符。
除此以外,咱們看到Script Execution步驟經過調用document.write迴流到Input Stream Preprocessor(這也是爲何腳本執行會阻塞瀏覽器解析的緣由),顧名思義在腳本執行階段,document.write插入的內容會在注入到Input Stream中,而且做爲下一個解析點。咱們來看看效果:
<!DOCTYPE html>
<html lang="en">
...
<body>
<p>
parser first
</p>
<script> document.write('<p>parser second</p>') </script>
<p>
parser third
</p>
</body>
</html>
複製代碼
能夠看到document.write調用的輸出要在腳本以後的標籤的前面。
Tokenizer在編譯器領域是比較常見的一個名詞,直譯過來就是令牌化的意思,咱們考慮一下HTML文檔中有多少種類型的字符:
而咱們接收到的是一串無狀態的字符串,爲了方便HTML解析,咱們須要將這一長串字符串,切分紅一系列子串,並打上相應的標籤,賦予對應的狀態,一個個的傳遞給Tree Construction,這就是Tokenizer的職責。
Tree Construction有一系列的插入狀態,確保node節點插入在合適的位置,若是一個節點出如今非法位置則會致使Parser Error(eg:在head內寫了個span標籤),Parser Error不必定會致使Parser終止,規範定義了一系列糾錯機制。
<!-- 一系列的插入狀態保證了html解析成dom能生成以下的結構 -->
<!DOCTYPE html>
<html lang="en">
<head></head>
<body></body>
</html>
<!DOCTYPE html>
<html lang="en">
<head>
<!-- 非法的節點類型,會致使Parser Error,根據糾錯機制會忽略該節點。 document.querySelector('#invalid')將會是null -->
<span id="invalid">2</span>
</head>
<body></body>
</html>
複製代碼
Tree Construction最終會產出一個node節點並插入到Dom中,這時候咱們就能夠經過JavaScript去操做Dom了。
這樣就完事了?好戲如今纔開始! 在剛剛的敘述過程當中,隱藏了對具體標籤的解析方式,<script src="index.js" />和<link href="index.css" />怎麼能同樣呢?
接下來咱們說說樣式、腳本在HTML Parser執行過程當中的具體表現。
坊間流傳的最廣的說法就是,樣式加載不會阻塞HTML解析,而腳本會。
這句話太過模糊,畢竟樣式有內聯樣式、外部樣式等,一樣腳本也有內聯腳本、外部腳本,而外部腳本又有defer、async這些屬性進行再次區分。
樣式的解析並不屬於HTML Parser的工做內容,對於HTML Parser而言只須要把link或者style標籤插入到Dom中就完事了,因此對於style和link標籤內的樣式資源的加載和解析工做是經過並行的方式去執行的。
所謂的並行就是將主線程(HTML Parser所在的線程)返回,並建立一個子線程(或其餘並行的實現方式eg:fiber),將接下來的任務(eg: 下載樣式和解析樣式)放到子線程中執行。
到此爲止,結論都指向樣式加載解析不阻塞HTML解析,大部分狀況下這個結論都是對的,說到這就要說下解析過程當中的一個全局變量script-blocking style sheet counter。
這個變量會在以下場景發生變化:
那麼什麼樣的樣式資源叫script-blocking style sheet呢?
// script-blocking style sheet
<link rel="stylesheet" type="text/css" href="./index.css"/>
<style> @import './index2.css'; </style>
複製代碼
如今只須要記住這個變量和腳本執行還有渲染有關係便可,具體聯繫咱們接着看腳本的加載模式。
這是一張流傳比較廣的script加載流程圖,基本涵蓋了大部分script加載對HTML Parser的影響,但還有部分細節的缺失,咱們看看規範是怎麼定義的。
對於沒有defer/async屬性的script,咱們稱之爲pending parsing-blocking script,但須要注意的是:
那麼pending parsing-blocking script是如何加載執行的呢?
經過上面步驟,咱們看到在這個場景下,樣式的加載解析是會阻塞pending parsing-blocking script的執行的,進而致使HTML Parser的阻塞。
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<!--在我解析完成以前,pending parsing-blocking script別想執行-->
<link rel="stylesheet" type="text/css" href="./index.css?lazy=1000" />
</head>
<body>
<!--我是pending parsing-blocking script,我會阻塞Parser, 但我要等到樣式解析完成後才能執行-->
<script src="./index.js"></script>
<!--我要等到👆的腳本執行完成後才能解析-->
<span>hello</span>
</body>
</html>
複製代碼
爲何要樣式解析要設計成阻塞這部分腳本執行的呢?考慮以下場景:
// index.css
.color {
color: red
}
複製代碼
<!DOCTYPE html>
<html lang="en">
<head>
<title>Document</title>
<link rel="stylesheet" type="text/css" href="index.css" />
</head>
<body>
<p class="color">個人顏色是什麼</p>
<script> const element = document.querySelector('.class'); const color = window.getComputedStyle(element).color; console.log(color) // rgb(255, 0, 0); // 若是script沒有等待css加載完畢就執行,會致使腳本獲取到錯誤樣式 </script>
</body>
</html>
複製代碼
咱們知道script標籤還有async和defer兩種影響它加裝執行的屬性,咱們看看具體表現又是如何。須要知道的幾點:
當HTML Parser遇到帶有defer標識且沒有async標識的script標籤,HTML Parser會將對應的script存在一個名爲 list of scripts that will execute when the document has finished parsing隊列中(有序),而且進行並行下載(不會阻塞主線程),但不會執行;而是等到HTML Parsing完成後,再回頭執行這個隊列,具體執行時機咱們後面還會講。
當HTML Parser遇到帶async標識的script標籤的時候,Parser依然會選擇把它存起來先,存在一個名爲set of scripts that will execute as soon as possible的集合中,而後開啓並行下載,與defer不一樣的是,當async script加載完成後,會馬上尋找機會執行(event loop next tick);這樣形成的結果是async script的運行時機不可預測,且是無序的(下載完的先執行);且當在HTML Parsing完成以前,async script下載完畢,依然會阻塞後續的Parser任務(可是async script下載期間不阻塞Parser)。
還有一種類型的script list名爲list of scripts that will execute in order as soon as possible,目前我探索出來的僅有以下狀況符合這種類型的腳本:
// 由JavaScript建立,且aysnc爲false的script element
const script = document.createElement('script')
script.async = false; // javascript 建立的script標籤async默認爲true
script.src = 'dy.js';
document.body.append(script);
複製代碼
對於這種類型的腳本,解析和執行形式和async相似,惟一不一樣的是這種類型的腳本,會按照添加的順序執行,而async script是無序的;另外這兩種腳本的執行都是不會被樣式表的解析所阻塞的。
到這裏咱們應當還有如下疑問:
HTML Parser在完成解析工做後,還有一些事項須要收尾,好比觸發一些事件,把沒跑的腳本給跑了等等;須要留意的是HTML Parser解析工做完成後不表明樣式表的解析工做完成了,畢竟解析樣式表的工做不屬於Parser,這意味着script-blocking style sheet counter有可能大於0。
一個前置知識document.readyState能夠是三個值之一:lodaing、interactive、complete,加載文檔時是loading,當狀態發生改變時會觸發document.onreadystatechange事件。
解析完HTML文本後Parser會執行如下步驟:
瞭解這些底層邏輯有助於咱們,在進行編譯時優化時擁有方向,處理首屏加載問題上能夠認識到是什麼在阻塞瀏覽器的加載。
其實對於script而言,最優的加載方式早就寫在各大論壇的各大博客上了,就是掛在HTML的最後,可是當咱們要作一些特殊操做的時候,忽然須要script不被放在頭部的樣式阻塞,或者並行加載一些依賴,這些知識就派上了用場。
說完這些,看完的同窗可能會發現文中頻繁出現event loop和任務的字眼,其實本質上HTML Parser就是跑在event loop上的一個任務,其實event loop的邏輯在HTML Standard中的規範至關複雜,它不只僅是調度執行JavaScript的任務,它基本調度了頁面中的全部任務。
個人下一篇文章應該會是根據HTML Standard的描述去解析event loop,其中會包括event loop調度模式,會包括原本中提到的任務暫停機制,還有你們應該感興趣的渲染時機的問題,其實本文沒有提到渲染的問題,但其實渲染可能出如今CSS加載完成後任意一個時機。
若是大夥以爲寫得還行,但願能點個贊,對下一篇感興趣能夠加個關注😁