1.引子
最近看到有道題討論document.write
在html檔案中不同位置時在頁面上的執行順序(題目在後面具體討論),於是寫這篇部落格討論一下瀏覽器的頁面渲染,在http請求中傳輸的位元組碼如何變成瀏覽器呈現在使用者面前的介面。
2.程式與執行緒
這似乎是個永恆的話題,很多人開始都是混淆的。對於這兩個概念,可以記住下面這幾條:
- 程式是cpu資源分配的最小單位(系統會給它分配記憶體)
- 程式之間相互獨立,對於瀏覽器,每開啟一個Tab頁都可以認為開了一個新的程式。
- 程式擁有自己的多執行緒,各個執行緒之間相互協作完成任務。
在這裡我們只討論一個頁面的渲染機制,即下面說的執行緒都在一個程式內。對於瀏覽器,開一個新頁面(程式)工作的是以下執行緒:
與瀏覽器頁面渲染有關的主要是和這裡的GUI引擎執行緒和JS引擎執行緒:
GUI引擎執行緒(後面我們說UI執行緒):解析html css,進行DOM
,CSSOM
,RenderTree
的繪製,迴流,重繪的執行者,以及頁面渲染都是由他來完成(難以避免的丟擲一大堆概念,後面會一一解釋)。- JS引擎執行緒:用來對js檔案進行處理。
- 上面兩個執行緒是互斥的(請記住這句話,很重要),當有一個在進行時,另外一個將被掛起,也就是說會造成阻塞,至於誰阻塞誰,後面再說。
3.當瀏覽器接收到伺服器發過來的資料包時...
- 將資料包進行解析。
- 解析html檔案,UI執行緒進行DOM樹的構建,此操作將確定節點的父子以及兄弟關係。
- 當繼續解析到類似的
<link rel='stylesheet' href='../example.css'/>
語句時將下載相應的css檔案,並進行CSSOM
的構建(類似DOM
的東西),將確定css屬性之間的級聯關係。 - 瀏覽器將
DOM
與CSSOM
進行合併,構建RenderTree
,所謂的渲染樹。然後瀏覽器會根據渲染樹進行名為reflow
(迴流)的過程,來根據瀏覽器頁面的具體情況確定各個節點的渲染位置(該操作會遍歷整個DOM
和CSSOM
,對效能影響很大)。 - 接下來就是將準備好的東西渲染到螢幕上了。以上過程可以用下面這張圖演示
上面的看似很順暢的過程,卻隱去了一個重大的問題(js呢)?
接上面談起,瀏覽器開始解析時碰到
<link rel='stylesheet' href='../example.css'/>
會開始下載css檔案,同樣當遇到語句時<script src='./example.js'></script>
js引擎會下載並執行js檔案,注意這裡會引起阻塞,阻塞具體情況如下:- 阻斷DOM的構建,應為瀏覽器不知道js檔案會對DOM進行什麼操作,也就是說JS執行會阻塞
DOM
構建。 - 如果
CSSOM
沒有就緒,那麼JS將等到CSSOM
準備就緒時再執行,也就是說CSSOM
的構建會阻塞JS執行,其實也就是間接的在阻塞DOM
的構建。 - 那麼我們是否能對JS的執行進行操作呢,答案是:可以.
- async屬性
<script src='./example.js' async></script>
- 它的作用是指定相應的js檔案在下載好再進行執行,也就是說這個js檔案在下載過程中是不阻塞
DOM
構建的。 - defer屬性
<script src='./example.js' defer></script>
- 指定對應js檔案在整個頁面都解析完成後再執行,此時
DOM
和CSSOM
都已經準備就緒。
說一組概念(很重要):
- 迴流(reflow)和重繪(repaint)
- 迴流:當
Render Tree
中部分或全部元素的尺寸、結構、或某些屬性發生改變時,瀏覽器重新渲染部分或全部文件。 - 重繪:當頁面中元素樣式的改變並不影響它在文件流中的位置時,瀏覽器會將新樣式賦予給元素並重新繪製它。
- 至於具體什麼操作會引起迴流以及重繪,這裡不一一列出。
- 迴流必定重繪,重繪不一定迴流。
- 很明顯:迴流的代價要比重繪高很多。在效能優化時有一點就是避免頻繁造成迴流。
4.那麼回到我們開篇的問題
先來介紹這條語句:
document.write
document.open()
開啟的一個文件流。注意: 因為
document.write
需要向文件流中寫入內容,因此在關閉(已載入)的文件上呼叫 document.write
會自動呼叫 document.open
,這將清空該文件的內容。<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
document.write('指令碼輸出');
</script>
</head>
<body>
<p>頁面內容</p>
<p>頁面內容</p>
</body>
</html>複製程式碼
頁面輸出結果:
//指令碼輸出
//頁面內容
//頁面內容複製程式碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
</head>
<body>
<p>頁面內容</p>
<script>document.write('指令碼輸出');</script> <p>頁面內容</p>
</body>
</html>複製程式碼
頁面輸出結果:
//頁面內容
//指令碼輸出
//頁面內容複製程式碼
以上的結果看似都是情理之中,document.write
會在執行到時將內容新增到DOM
樹中。
而當我們把這條語句放到</body>
或者</html>
之後時,瀏覽器的做法都是將這條語句提升到</body>
之前執行,下面是谷歌還有火狐,IE的截圖。
- chrome
firefox
IE
還有就是將上面的document.write
放在onload
時執行,像下面這樣:
<!DOCTYPE html><html lang="en">
<head>
<meta charset="UTF-8">
<title>Document</title>
<script>
window.onload=function(){
document.write('指令碼輸出');
}
</script>
</head>
<body>
<p>頁面內容</p>
<p>頁面內容</p>
</body>
</html>複製程式碼
結果就是,不論script標籤在哪,頁面上都只會渲染指令碼輸出
一句話,所以我們得到下面結論:
- 瀏覽器解析
DOM
到body
標籤為止 body
標籤之後的script
會被提升到</body>
之前執行,但仍在onload
事件之前。
後記:
根據個人理解以及整理得,有錯誤或者偏差敬請原諒,歡迎指正。