當我們在瀏覽器當中輸入網址後,會發生什麼事?
- 透過 DNS server 找到 IP address
- 建立 TCP/IP 連線 (3-way handshake)
當通訊連線確認之後建立,就會正式送出 HTTP 請求。
HTTP 本身就是一種協議,全名是 Hypertext Transfer Protocol 超文本傳輸協議,當年由 Tim Berners-Lee 發起,提供一個可以發布和接受 HTML 文件的方法,其實也就是發布與接收網頁資訊的方法。
建立通訊連線之後, 瀏覽器 (client) 就可以發出 HTTP message 向 server 請求資源,這動作也就是發出 HTTP request。而 server 收到請求後,會提供相對應的資源,透過 HTTP message 回傳給 client,這動作就是收到 HTTP response。
Render process
接下來,就要來看看瀏覽器是怎麼將 HTTP response body 當中的內容,轉換成我們看到的網頁畫面!
在 Chrome browser process 當中的 network thread 完成連線,並收到 HTTP response 之後,若確認內容為 HTTP 文件,接下來,就會將任務交給 Chrome 當中的 render process 來處理畫面的呈現,render process 也會開始下載相關資源。
Create DOM tree
第一步,會將 HTML 轉換成 DOM
第一步,會將 HTML 轉換成 DOM (Document Object Model)。DOM 是瀏覽器所產生出來的資料結構,一方面表現出 HTML 的內容與架構,另一方面讓開發者有機會使用 JavaScript 透過 DOM 來操作頁面。
除了 HTML 文件本身之外,通常 HTML 文件也會同時載入其他資源,像是 CSS 或是 JavaScript。為了加速載入的時間,瀏覽器在載入 HTML 的時候,同步執行 “preload runner”,如果有看到 <img> 或是 <link> 等需要載入資源,就會提早送出請求並開始載入。
不過 <script> 的狀況就不太一樣。在載入 HTML 的過程中,如果看到 <script> 的話,會先暫停 HTML 的載入,然後開始下載並執行 <script> 當中的內容,原因是 <script> 裡面的內容可能會影響到 DOM tree 的內容,所以如果「載入 HTML」和 「執行 script (JavaScript)」同時發生的話,就有可能產生錯誤。</script>
也因此,<script> 在 HTML 文件中出現的位置,就相當的重要。除了可以透過調整 <script> 的位置來控制載入和執行的時間點之外,也可以透過 defer 和 async 屬性來調整其載入和執行的時間點。關於 defer 和 async 的細節,可以參考 </script>前端三十|02. [HTML] script tag 加上 async & defer 的功能及差異? 這篇文章的說明。
Computing style
把 HTML 轉換成 DOM tree 之後,接下來,就要加入 CSS style 讓畫面不只有內容,而是更加美觀。我們會在 CSS 文件當中,透過許多的 “selectors” 來選擇並定義 HTML/DOM 當中元素的樣式,所以要能夠在最後準確呈現我們想要的內容,瀏覽器就需要去 根據 CSS 文件上的內容,”計算” 這些樣式,並放到對應的 DOM 元素上。
在這個過程中,瀏覽器會產生出一個另外一個資料結構,叫做 CSSOM (CSS Object Model)
接下來,瀏覽器會將 DOM tree 和 CSSOM tree 整合在一起,變成 Render tree。如果在載入 CSS 文件時遇到問題或是延緩,就會影響到後續畫面的呈現,因為瀏覽器不會在完成 Render tree 之前,就呈現畫面。
完成 Render tree 後,事情還沒有結束,這時候我們只是先把材料準備好而已。之後,瀏覽器才要正式的在畫面上畫出東西。
如果想要看到網頁開啟時載入了哪些資源,可以打開 Chrome DevTool 當中的 Source 標籤,看到載入的文件;可以在 Network 標籤當中,看到不同時間點資源的載入。另外,也可以在 Elments 標籤當中的 Computed (在 Styles 右邊) ,看到某個元素最後計算出來的 CSS 樣式的完整內容。
學會透過 JavaScript 操作 DOM,打造具有豐富互動的網頁
Virtual DOM
Virtual DOM,中文翻譯叫做虛擬 DOM,顧名思義,他不是一個真正的 DOM。DOM 是由瀏覽器所產生,並存在在瀏覽器當中的資料結構,而 Virtual DOM 本身是一個 JavaScript 的物件,存在在 memory 當中。
為了避免每次互動引發整個 DOM 的改變,進而產生不必要的 reflow 或 repaint,前端框架在設計與實作上,會先將原本的 DOM 結構複製一份出來,但不會完整複製 DOM 當中的所有資訊,只會複製跟畫面渲染高度相關的資訊,譬如 tag (HTML tag)、props (tag 當中的資訊)與 children (子結點資訊) 等,利用 JavaScrip 物件的資料結構來儲存。
每當有事件產生,或是資料變動的時候,前端框架會先建立一個新的 virtual DOM,接著,計算出新舊 virtual DOM 之間的差別,最後才會操作真正的 DOM,並僅僅操作有變動的部分,藉此避免不必要的 reflow 或 repaint 以提升效能。
不過這裡我們也會發現,操作 virtual DOM 應為會帶來另外一個效能上的負擔,因為在實際改變 DOM 之前,會經歷建立 virtual DOM、計算差異、實際操作 DOM 的過程。只不過,操作 virtual DOM 實際上就是在操作純 JavaScript,因此在大部分的情況下,會比不夠過 virtual DOM 直接操作真正的 DOM 還要來得快許多。
Diffing
不過,前端框架是如何「計算」出差異,並降低 DOM 當中需要修改的規模呢?
若以 React 為例,要遍歷整個 virtual DOM tree 並計算出差異,會需要 O(n^3) 的時間複雜度,因此 React 做了兩個假設:
- 兩個不同類型的 HTML element 會產生不同的 tree
- 開發者可以透過 “key” 的標記,來判斷畫面有無需要更動
因此當 React 在遍歷 virtual DOM tree 的時候,如果發現 element 類型 (tag) 不同的話,就會把該 element 以下的整個子 tree 砍掉重練,就不需要花時間再往下遍歷了。當然若遇到同樣類型的 element,就會實際去檢查當中的屬性與資料,並繼續往下(子結點)遍歷。
另一方面,透過 “key” 的標記,可以知道哪些 elements 是原本就存在在舊 virtual DOM tree 之中,因此如果只是在同一層的位置改變,就不需要重新計算修改,藉此提升整體效能。
關於 Diffing 演算法的細節,就不在這裡深入探究,有興趣的話可以參考Reconciliation與虛擬 DOM 及 diff 算法 等文章。
(本文轉載自ALPHA Camp 助教TD的 前端開發 30 個問題 系列文)