進(jìn)程和線程
進(jìn)程(process)和線程(thread)是操作系統(tǒng)的基本概念。
現(xiàn)代操作系統(tǒng)都是可以同時運(yùn)行多個任務(wù)的,比如:用瀏覽器上網(wǎng)的同時還可以聽音樂。對于操作系統(tǒng)來說,一個任務(wù)就是一個進(jìn)程,比如打開一個瀏覽器就是啟動了一個瀏覽器進(jìn)程,打開一個 Word 就啟動了一個 Word 進(jìn)程。
有些進(jìn)程同時不止做一件事,比如 Word,它同時可以進(jìn)行打字、拼寫檢查、打印等事情。在一個進(jìn)程內(nèi)部,要同時做多件事,就需要同時運(yùn)行多個“子任務(wù)”,我們把進(jìn)程內(nèi)的這些“子任務(wù)”稱為線程。
由于每個進(jìn)程至少要做一件事,所以一個進(jìn)程至少有一個線程。
系統(tǒng)會給每個進(jìn)程分配獨立的內(nèi)存,因此進(jìn)程有它獨立的資源。同一進(jìn)程內(nèi)的各個線程之間共享該進(jìn)程的內(nèi)存空間(包括代碼段,數(shù)據(jù)集,堆等)。
如果電腦是 windows 系統(tǒng),打開任務(wù)管理器,可以看到有一個后臺進(jìn)程列表,在這里我們可以看到每個進(jìn)程的內(nèi)存資源信息以及 CPU 占有率。

我們再用官方的術(shù)語描述一下:
進(jìn)程是 CPU 資源分配的最小單位(是能擁有資源和獨立運(yùn)行的最小單位)。
線程是 CPU 調(diào)度的最小單位(是建立在進(jìn)程基礎(chǔ)上的一次程序運(yùn)行單位)。
瀏覽器是多進(jìn)程的
理解了進(jìn)程和線程之后,接下來我們對瀏覽器進(jìn)行一定程度上的認(rèn)識。
瀏覽器是多進(jìn)程的,每打開一個 tab 頁,就相當(dāng)于創(chuàng)建了一個獨立的瀏覽器進(jìn)程。

圖中打開了 Chrome 瀏覽器的多個 tab 頁,在 Chrome 任務(wù)管理器中可以看到有多個進(jìn)程,每一個 tab 頁有一個獨立的進(jìn)程。
注意:瀏覽器應(yīng)該也有自己的優(yōu)化機(jī)制,有時候打開多個 tab 頁,在 Chrome 任務(wù)管理器中會看到有些進(jìn)程被合并了,所以每個 tab 頁對應(yīng)一個進(jìn)程并不一定是絕對的。
瀏覽器包含哪些進(jìn)程?
為了簡化理解,這里僅列舉主要進(jìn)程。
-
Browser 進(jìn)程:瀏覽器的主進(jìn)程,只有一個。
負(fù)責(zé)瀏覽器界面的顯示與交互;
各個頁面的管理,創(chuàng)建和銷毀其他進(jìn)程;
網(wǎng)絡(luò)的資源管理、下載等。
Renderer 進(jìn)程:也稱為瀏覽器渲染進(jìn)程或瀏覽器內(nèi)核,內(nèi)部是多線程的。主要負(fù)責(zé)頁面渲染,腳本執(zhí)行,事件處理等。
第三方插件進(jìn)程:每種類型的插件對應(yīng)一個進(jìn)程,僅當(dāng)使用該插件時才創(chuàng)建。
GPU 進(jìn)程:最多一個,用于 3D 繪制等。
瀏覽器多進(jìn)程的優(yōu)勢
由于默認(rèn) 新開 一個 tab 頁面 新建 一個進(jìn)程,所以單個 tab 頁面崩潰不會影響到整個瀏覽器;
同樣,第三方插件崩潰也不會影響到整個瀏覽器;
多進(jìn)程可以充分利用現(xiàn)代 CPU 多核的優(yōu)勢;
方便使用沙盒模型隔離插件等進(jìn)程,提高瀏覽器的穩(wěn)定性。
系統(tǒng)為瀏覽器新開的進(jìn)程分配內(nèi)存、CPU 等資源,所以內(nèi)存和 CPU 的資源消耗也會更大。
瀏覽器內(nèi)核(渲染進(jìn)程)
前面說了這么多的進(jìn)程,對普通前端操作來說,最重要的還是渲染進(jìn)程。
瀏覽器的渲染進(jìn)程是多線程的,頁面的渲染,JS的執(zhí)行,事件的循環(huán)等,都在這個進(jìn)程內(nèi)執(zhí)行。
渲染進(jìn)程通常由以下常駐線程組成:
1. GUI 渲染線程
負(fù)責(zé)渲染瀏覽器界面,解析 HTML、CSS,構(gòu)建 DOM tree和 render tree,布局和繪制等。當(dāng)界面需要重繪(repaint)或由于某種操作引發(fā)回流(reflow)時,該線程就會執(zhí)行。
2. JS 引擎線程
也稱為 JS 內(nèi)核,負(fù)責(zé)解析 JavaScript 腳本,運(yùn)行代碼。
-
JavaScript 是單線程的。
JavaScript 為什么是單線程的?這與它的用途有關(guān)。JavaScript 作為瀏覽器腳本語言,主要用途是與用戶互動以及操作 DOM。這也決定了它只能是單線程的,否則會帶來很復(fù)雜的同步問題。想想一下,如果 JavaScript 同時有連個線程,一個線程在某個 DOM 節(jié)點上添加內(nèi)容,另一個線程刪除了這個 DOM 節(jié)點,這時瀏覽器應(yīng)該以哪個線程為準(zhǔn)呢?所以,為了避免復(fù)雜性,JavaScript 從一開始就是單線程。
-
GUI 渲染線程 與 JS 引擎線程是互斥的。
由于 JavaScript 可以操作 DOM,如果在修改元素屬性的同時渲染界面(即 JavaScript 引擎線程和 GUI 渲染線程同時運(yùn)行),那么渲染線程前后獲得的元素數(shù)據(jù)就可能會不一致。因此,為了防止渲染出現(xiàn)不可預(yù)期的結(jié)果,瀏覽器設(shè)置 GUI 渲染線程與 JS 引擎為互斥的關(guān)系。當(dāng) JS 引擎執(zhí)行時,GUI 線程被掛起,GUI 更新被保存在一個隊列中,等到 JS 引擎線程空閑時立即被執(zhí)行。
-
JS 阻塞頁面加載。
由于 GUI 渲染線程與 JS 引擎線程是互斥的,當(dāng)瀏覽器在執(zhí)行 JavaScript 的時候,GUI 渲染線程會被保存在一個隊列中,直到 JS 程序執(zhí)行完成,才會接著執(zhí)行。因此如果 JS 執(zhí)行時間過長,就會造成頁面的渲染不連貫,導(dǎo)致頁面渲染加載阻塞。
3. 事件觸發(fā)線程
當(dāng)一個事件被觸發(fā)時,該線程會把事件添加到待處理隊列的隊尾,等待 JS 引擎處理。這些事件可以是當(dāng)前執(zhí)行的代碼塊,如定時任務(wù);也可以是來自瀏覽器內(nèi)核的其他線程,如:鼠標(biāo)點擊、Ajax異步請求等。但由于 JS 是單線程的,這些事件都需要排隊等待 JS 引擎處理。
4. 定時觸發(fā)器線程
setTimeout 和 setInterval 所在的線程。瀏覽器定時計數(shù)器并不是由 JS 引擎計數(shù)的,因為 JS 是單線程的,如果處于阻塞線程狀態(tài)就會影響計時的準(zhǔn)確,所以通過單獨的線程來計時并觸發(fā)定時更為合理。
5. 異步 http 請求線程
XMLHttpRequest 在建立連接后,通過瀏覽器新開一個線程請求,一旦檢測到狀態(tài)變更并且設(shè)置有回調(diào)函數(shù),異步線程就產(chǎn)生狀態(tài)變更事件,將這個回調(diào)再放入事件隊列中,等待 JS 引擎空閑時處理。
Browser 進(jìn)程和 Renderer 進(jìn)程的通信過程
打開瀏覽器的一個 tab 頁時,我們看下其中的大致過程:
Browser 進(jìn)程收到用戶請求,通過網(wǎng)絡(luò)下載獲取頁面內(nèi)容,然后將該任務(wù)通過RendererHost接口傳遞給 Renderer 進(jìn)程;
-
Renderer 進(jìn)程的 Renderer 接口收到消息,簡單解釋后,交給 GUI 渲染線程開始渲染;
GUI 渲染線程接收請求,加載網(wǎng)頁并渲染網(wǎng)頁,這個過程中可能需要 Browser 進(jìn)程獲取資源和 GPU 進(jìn)程來幫助渲染,也可能會有 JS 引擎線程操作 DOM(可能造成回流并重繪);
最后 Renderer 進(jìn)程將結(jié)果傳遞給 Browser 進(jìn)程;
Browser 進(jìn)程接收到結(jié)果,并將結(jié)果繪制出來。
到這里應(yīng)該對瀏覽器的運(yùn)作有一定理解了,我們再來看下瀏覽器是怎么渲染頁面的。
瀏覽器的渲染流程
瀏覽器內(nèi)核拿到頁面內(nèi)容后,渲染過程大概分為以下幾個部分:

- 解析 HTML 文件,生成 DOM tree;同時解析 CSS 文件以及樣式元素中的樣式數(shù)據(jù),生成 CSS Rules。
- 構(gòu)建 render tree:根據(jù) DOM tree 和 CSS Rules 來構(gòu)建 render tree,它可以讓瀏覽器按照正確的順序繪制內(nèi)容。
- 布局(layout / reflow):計算各元素尺寸、位置。
- 繪制(paint):繪制頁面像素信息。
- 瀏覽器將各層信息發(fā)送給 GPU,GPU 將各層信息合成(composite),顯示在屏幕上。
補(bǔ)充:
Webkit 將 render tree 中的元素稱為 render object (或 renderer),每一個 render object 都代表一個的矩形區(qū)域,通常對應(yīng)于相關(guān)節(jié)點的 CSS 框,這些矩形的排列順序就是它們在屏幕上顯示的順序。
Render object 和 DOM 節(jié)點是相對應(yīng)的,但并非一一對應(yīng)。非可視化的 DOM 元素不會插入 render tree 中,例如“head”元素 和 一些 display: none 的節(jié)點就沒必要放在 render tree 中了。
這里只是大致的過程,詳細(xì)步驟可以看參考資料中的第一篇。
渲染完成后,接下來就是 JavaScript 邏輯處理了。