前言
現(xiàn)代瀏覽器十分復(fù)雜,頗有運(yùn)行在操作系統(tǒng)之上的"操作系統(tǒng)"的意思,我們將盡可能用簡(jiǎn)單容易理解的例子來(lái)簡(jiǎn)單概括它主要的工作邏輯。

目錄:
- 進(jìn)程與線程概述;
- 瀏覽器架構(gòu);
- 瀏覽器視角下的輸入;
- 頁(yè)面如何渲染;
- 如何進(jìn)行交互;
Part 1. 進(jìn)程與線程概述
計(jì)算機(jī)的核心是 CPU,它承擔(dān)了幾乎所有的計(jì)算任務(wù)。

你可以把 CPU 想象成是一個(gè)工廠,時(shí)刻在運(yùn)行著。
假設(shè)這個(gè)工廠的電力有限,同一時(shí)刻只能供一個(gè)車間使用。這也就意味著,一個(gè)車間正在使用,其他車間都將不會(huì)被使用。

進(jìn)程就好比車間,是工廠將要執(zhí)行的任務(wù)。潛臺(tái)詞就是說(shuō),單個(gè) CPU 任意時(shí)刻總是只能運(yùn)行一個(gè)任務(wù)。
一個(gè)車間可以有很多的工人,它們協(xié)同完成同一個(gè)任務(wù)。

線程就是車間里的工人。
假設(shè)工人都是很耗電的機(jī)器人,靠著分得工廠給的電力進(jìn)行任務(wù),每一次給的電力剛好夠完成本次的任務(wù),而工廠同一時(shí)刻又只能給一個(gè)機(jī)器人供電。

這幾乎就是單核 CPU 的工作方式了:同一時(shí)刻只能做一個(gè)工作。
但你仍然感覺(jué)到許多不同的任務(wù)正在 "同時(shí)" 運(yùn)行著,這是因?yàn)楫?dāng)切換任務(wù)的速度足夠快時(shí),你將感知不到 CPU 同一時(shí)刻只能做一個(gè)工作的特性:

我們的 CPU 就這樣飛速地奔騰著。
每當(dāng)我們打開(kāi)一個(gè)應(yīng)用,就會(huì)啟動(dòng)一個(gè)進(jìn)程。程序也會(huì)創(chuàng)建一個(gè)或多個(gè)線程來(lái)幫助它完成工作。
操作系統(tǒng)會(huì)為進(jìn)程提供一個(gè)可使用的 "一塊" 內(nèi)存,就像開(kāi)工廠占地一樣,所有應(yīng)用程序的狀態(tài)信息都會(huì)保存在該私有內(nèi)存空間中。程序關(guān)閉時(shí),相應(yīng)進(jìn)程會(huì)消失,操作系統(tǒng)也會(huì)釋放內(nèi)存。

進(jìn)程可以請(qǐng)求操作系統(tǒng)啟動(dòng)另一個(gè)進(jìn)程來(lái)執(zhí)行不同的任務(wù)。此時(shí)內(nèi)存不同區(qū)域會(huì)分給新進(jìn)程。
如果兩個(gè)進(jìn)程需要對(duì)話,他們可以通過(guò) 進(jìn)程間通信(IPC) 來(lái)進(jìn)行。

許多應(yīng)用程序就是這樣設(shè)計(jì)的,如果一個(gè)工作進(jìn)程失去響應(yīng),該進(jìn)程就可以在不停止應(yīng)用程序的情況下靠著其他進(jìn)程重新啟動(dòng)。
Part 2. 瀏覽器架構(gòu)
那么如何通過(guò)進(jìn)程和線程構(gòu)建 web 瀏覽器呢?

雖然對(duì)于如何構(gòu)建 web 瀏覽器沒(méi)有明確的標(biāo)準(zhǔn),但現(xiàn)在擁有一個(gè)導(dǎo)航欄、輸入框、標(biāo)簽頁(yè)這樣類似的設(shè)計(jì)卻是不同瀏覽器之間默契的共同選擇。
瀏覽器的架構(gòu)也總體分為兩類:

現(xiàn)在已經(jīng)很難看到單進(jìn)程的架構(gòu)方式了,因?yàn)閱芜M(jìn)程的瀏覽器需要處理的事情太多(網(wǎng)絡(luò)、渲染、管理插件等),極不穩(wěn)定和安全。因此市面上主流的瀏覽器都已經(jīng)升級(jí)為多進(jìn)程的方式。
就拿 Chrome 舉例來(lái)說(shuō),就采取了下方的架構(gòu)方式:

- 最頂層是瀏覽器進(jìn)程,負(fù)責(zé)協(xié)調(diào)處理其他進(jìn)程模塊的任務(wù)。
- UI 進(jìn)程負(fù)責(zé)控制地址欄、標(biāo)簽頁(yè)等;
- 渲染進(jìn)程控制標(biāo)簽頁(yè)內(nèi)網(wǎng)站的展示。
- 插件進(jìn)程控制站點(diǎn)使用的任意插件,比如:Flash。
- GPU 進(jìn)程單獨(dú)處理來(lái)自不同應(yīng)用發(fā)送的繪制請(qǐng)求。
- ....
多進(jìn)程的好處顯而易見(jiàn)。比如當(dāng)你打開(kāi)了三個(gè)標(biāo)簽頁(yè),其中一個(gè)崩潰了,你可以關(guān)掉它而不會(huì)影響其他兩個(gè)標(biāo)簽頁(yè):

并且由于進(jìn)程的數(shù)據(jù)是私有的,所以一定程度上能夠保證安全性。
但缺點(diǎn)也顯而易見(jiàn)。我們上面用車間來(lái)類比進(jìn)程,用工人來(lái)類比線程,顯然「建一座車間」比「招聘一個(gè)工人」消耗的資源要大得多——哪怕車間只有一個(gè)工人——這里比較明顯的是對(duì)內(nèi)存的消耗。

為了避免過(guò)大的內(nèi)存消耗,Chrome 把一些服務(wù)做了聚合:

這樣就能一定程度上減少內(nèi)存的開(kāi)銷。
Part 3. 瀏覽器視角下的輸入
當(dāng)在瀏覽器中鍵入一個(gè) URL 地址,瀏覽器會(huì)做什么處理呢?

第一步:處理輸入
我們已經(jīng)習(xí)慣了一個(gè)鏈接打開(kāi)就對(duì)應(yīng)一個(gè)外部網(wǎng)站,但它還可能是瀏覽器本身的設(shè)置頁(yè)(如 chrome://settings/),或是本地硬盤的地址(如 Mac 下的 \):

所以我們的第一步就是要判斷這個(gè)輸入到底是個(gè)啥:

第二步:開(kāi)始導(dǎo)航
隨著用戶輸入完畢按下 Enter 鍵,UI 線程知道要啟用網(wǎng)絡(luò)去調(diào)取網(wǎng)站的信息。網(wǎng)絡(luò)線程會(huì)負(fù)責(zé)聯(lián)系目標(biāo)主機(jī)并獲取到信息:

網(wǎng)絡(luò)線程獲取信息的過(guò)程,發(fā)生了很多事,比如 DNS 域名解析、TLS 建立連接等,如果不熟悉可以看看之前的系列文章。
第三步:讀取響應(yīng)
總之網(wǎng)絡(luò)線程為我們?nèi)〉搅藖?lái)自網(wǎng)站的響應(yīng),大概長(zhǎng)這樣:

響應(yīng)分為 header 和 payload 兩個(gè)部分。header 類似于一本書(shū)的版權(quán)、作者介紹等相關(guān)信息,而 payload 才是真實(shí)的數(shù)據(jù)內(nèi)容。
瀏覽器需要根據(jù)響應(yīng)頭里的 Content-Type 來(lái)區(qū)分對(duì)應(yīng)內(nèi)容的類型,例如 text/html 時(shí)瀏覽器會(huì)對(duì)內(nèi)容進(jìn)行 HTML 解析,image/png 則調(diào)用圖片渲染器。
然而完全信任網(wǎng)站響應(yīng)的 Content-Type 是不行的,因?yàn)橐坏?Content-Type 未指定或者是一個(gè)錯(cuò)誤的值的時(shí)候,就會(huì)發(fā)生未知的錯(cuò)誤。
所以當(dāng)收到響應(yīng)主體(payload)時(shí),網(wǎng)絡(luò)線程會(huì)在必要時(shí)檢查數(shù)據(jù)的前幾個(gè)字節(jié),以確保數(shù)據(jù)內(nèi)容與 header 里標(biāo)識(shí)的數(shù)據(jù)類型(Content-Type)一致。如果不一致,那么就需要進(jìn)行 MIME 類型嗅探來(lái)猜測(cè)該數(shù)據(jù)的類型。

當(dāng)響應(yīng)是一個(gè) HTML 文件時(shí),此時(shí)也會(huì)進(jìn)行安全檢查(SafeBrowsing 檢查)。如果域名和相應(yīng)數(shù)據(jù)似乎匹配到了一個(gè)已知的惡意網(wǎng)站,那么網(wǎng)絡(luò)線程會(huì)顯示一個(gè)警告頁(yè)面。
除此之外,還會(huì)發(fā)生 Cross Origin Read Blocking(CORB)檢查,以確保敏感的跨域數(shù)據(jù)不被傳給渲染進(jìn)程。
第四步:查找渲染進(jìn)程
一旦所有的檢查執(zhí)行完畢并且網(wǎng)絡(luò)線程確信瀏覽器會(huì)導(dǎo)航到請(qǐng)求的站點(diǎn),網(wǎng)絡(luò)線程會(huì)告訴 UI 線程所有的數(shù)據(jù)準(zhǔn)備完畢。UI 線程會(huì)尋找渲染進(jìn)程去開(kāi)始渲染 web 頁(yè)面。

由于網(wǎng)絡(luò)請(qǐng)求會(huì)花費(fèi)幾百毫秒才獲取回響應(yīng),因此可以應(yīng)用一個(gè)優(yōu)化措施。
當(dāng)?shù)?2 步 UI 線程正發(fā)送一個(gè) URL 請(qǐng)求給網(wǎng)絡(luò)線程時(shí),它已經(jīng)知道它們會(huì)導(dǎo)航到哪個(gè)站點(diǎn)。在網(wǎng)絡(luò)請(qǐng)求的同時(shí),UI 線程并行地嘗試主動(dòng)尋找或開(kāi)啟一個(gè)渲染進(jìn)程。
這樣,如果一切按預(yù)期進(jìn)行,渲染進(jìn)程在網(wǎng)絡(luò)線程接受到數(shù)據(jù)時(shí)就已經(jīng)處于待命狀態(tài)。
第五步:提交導(dǎo)航
現(xiàn)在數(shù)據(jù)和渲染進(jìn)程已經(jīng)就緒,瀏覽器進(jìn)程會(huì)發(fā)送一個(gè) IPC(進(jìn)程間通信)到渲染進(jìn)程去提交導(dǎo)航。

這時(shí)地址欄會(huì)更新、標(biāo)簽頁(yè)的歷史記錄也會(huì)更新,前進(jìn)/后退按鈕會(huì)走向剛導(dǎo)航過(guò)的站點(diǎn)。渲染進(jìn)程根據(jù) HTML 內(nèi)容開(kāi)始解析并渲染頁(yè)面。最終您將看到網(wǎng)站設(shè)計(jì)者設(shè)計(jì)的網(wǎng)站。
Part 4. 頁(yè)面如何渲染
渲染進(jìn)程涉及 Web 性能的許多方面,流程非常復(fù)雜,我們只做必要的理解。如果您想要深入了解,可以在 web.dev 找到相關(guān)資源。

渲染進(jìn)程內(nèi)部包含主線程、工作線程、合成線程和光柵線程。
在詳細(xì)說(shuō)明之前,請(qǐng)先想象一個(gè)這樣的場(chǎng)景:您站在一副簡(jiǎn)單繪畫的面前,如何通過(guò)打電話來(lái)讓您的朋友知道這幅畫究竟長(zhǎng)什么樣子呢?

如果您真打算這么做,這里參考 HTML 解析的過(guò)程給您提供一些建議。
首先,圖中的元素以及具體元素的屬性分開(kāi)描述(如:圖里有一個(gè)圓是元素,圓有多大具體在什么位置等是屬性):

這樣做的好處是可閱讀性變高了,有哪些元素,以及元素哪些屬性一目了然,也利于分別維護(hù)和修改。(類似于書(shū)的目錄和對(duì)應(yīng)內(nèi)容一樣)

另外是你可以提煉一些通用的屬性來(lái)減少描述:

然后,最好是分層進(jìn)行描述,因?yàn)閳D畫是有層次的,光有元素大小、位置等信息是不夠的:

元素實(shí)際上就是我們通常說(shuō)的 HTML 文件,HTML 文件中包含了描述元素屬性的 CSS 樣式文件。每個(gè)瀏覽器對(duì)應(yīng)常見(jiàn)的樣式都會(huì)有默認(rèn)的樣式。
瀏覽器實(shí)際上要知道繪制些什么元素,每個(gè)元素屬性如何是要分成三步的:1)通過(guò) HTML 繪制元素樹(shù)(俗稱 DOM 樹(shù));2)通過(guò) CSS 文件繪制樣式樹(shù)(俗稱 CSSOM 樹(shù));3)綜合兩顆樹(shù)繪制渲染樹(shù)(俗稱 Render Tree);

現(xiàn)在瀏覽器知道文檔的結(jié)構(gòu)、每個(gè)元素的樣式、頁(yè)面的幾何形狀和繪制順序,它是如何繪制頁(yè)面的?把這些信息轉(zhuǎn)換為屏幕上的像素,我們稱為光柵化。

處理這種情況的一種簡(jiǎn)單的方法是,先在光柵化視窗內(nèi)的畫面,如果用戶滾動(dòng)頁(yè)面,則移動(dòng)光柵框,并光柵化填充缺少的部分。這就是 Chrome 首次發(fā)布時(shí)處理光柵化的方式。
但是,現(xiàn)代瀏覽器會(huì)運(yùn)行一個(gè)更復(fù)雜的過(guò)程,我們稱為合成。

合成是一種將頁(yè)面的各個(gè)部分分層,分別光柵化,并在稱為合成線程的單獨(dú)線程中合成為頁(yè)面的技術(shù)。如果發(fā)生滾動(dòng),由于圖層已經(jīng)光柵化,因此它所要做的只是合成一個(gè)新幀。動(dòng)畫也可以以相同的方式(移動(dòng)圖層和合成新幀)實(shí)現(xiàn)。
另外需要說(shuō)明的是如何進(jìn)行描述是有相當(dāng)?shù)募记傻?/strong>。例如「正中心有一個(gè) 半徑為 2 的圓」和「正中心有一個(gè) 直徑為頁(yè)面寬度 50% 的圓」是完全不同的:

如何進(jìn)行組織描述,這需要網(wǎng)站建設(shè)者的經(jīng)驗(yàn)。
Part 5. 如何進(jìn)行交互
在瀏覽器眼中,用戶的一切行為都是輸入。不單單是滾動(dòng)鼠標(biāo)滑輪,或是點(diǎn)擊屏幕、按下按鍵等。

對(duì)于瀏覽器進(jìn)程來(lái)說(shuō)只存在事件和對(duì)應(yīng)坐標(biāo),只有渲染進(jìn)程知道頁(yè)面究竟長(zhǎng)啥樣,以及究竟該如何處理事件。瀏覽器進(jìn)程只負(fù)責(zé)把事件和坐標(biāo)發(fā)送給渲染進(jìn)程。
我們也可以編寫自己的邏輯文件(js 文件)來(lái)監(jiān)聽(tīng)某一事件進(jìn)行對(duì)應(yīng)的處理。然后再統(tǒng)一由渲染進(jìn)程進(jìn)行合成。為了瀏覽流暢,瀏覽器需要保證渲染進(jìn)程的渲染速度與屏幕刷新率一致(大概每秒 60 幀)。
[站外圖片上傳中...(image-cac9e8-1613649340231)]
另外為了降低主線程中傳遞過(guò)量的調(diào)用,Chrome 也會(huì)把一些連續(xù)的事件進(jìn)行合并。
瀏覽器進(jìn)程監(jiān)聽(tīng)并發(fā)送事件給渲染進(jìn)程進(jìn)行渲染,這大概就是瀏覽器交互的基本方式。
后記
瀏覽器的復(fù)雜遠(yuǎn)不是一篇文章能解釋清楚的,本篇文章也只是想讓大家理解瀏覽器的基本過(guò)程和原理。盡可能使用動(dòng)圖的形式清晰地表達(dá),希望大家能用餐愉快。

本文大量借鑒了 Chrome 官方 developer 分享的系列文章(下2),如果有想更加深入了解的小伙伴也可以閱讀更加硬核的瀏覽器工作原理揭秘文章(下4)
至此,我們對(duì)瀏覽器已經(jīng)有了相當(dāng)?shù)牧私饬?。后續(xù)也會(huì)繼續(xù)跟大家一起學(xué)習(xí)計(jì)算機(jī)網(wǎng)絡(luò)的基礎(chǔ)知識(shí),也會(huì)嘗試著跟著后端學(xué)習(xí)路線圖的腳步跟著大家一起學(xué)習(xí)進(jìn)階。
[站外圖片上傳中...(image-9c5f9d-1613649340231)]
這里是我沒(méi)有三顆心臟,歡迎關(guān)注公眾號(hào) wmyskxz,2021,與您在 Be Better 的路上共同成長(zhǎng)!
參考資料
- 進(jìn)程與線程的簡(jiǎn)單解釋 - http://www.ruanyifeng.com/blog/2013/04/processes_and_threads.html
- 轉(zhuǎn)載:現(xiàn)代瀏覽器內(nèi)部揭秘 - https://hasaki.xyz/blog/2020-01-20-%E8%BD%AC%E8%BD%BD%E7%8E%B0%E4%BB%A3%E6%B5%8F%E8%A7%88%E5%99%A8%E5%86%85%E9%83%A8%E6%8F%AD%E7%A7%98/
- 深入淺出瀏覽器渲染原理 - https://blog.fundebug.com/2019/01/03/understand-browser-rendering/
- 瀏覽器工作原理幕后揭秘 - https://www.html5rocks.com/zh/tutorials/internals/howbrowserswork/
(完)